SlideShare a Scribd company logo
1 of 107
Download to read offline
http://tinyurl.com/sf-rnt
Unit
Tests
Practical
Andrew Fray, Spry Fox
#pracunittests
Test Driven
Development
#pracunittests
Backwards Is Forward: Making Better
Games with Test-Driven Development
http://gdcvault.com/play/1013416/Backwards-Is-Forward-Making-Better
Sean Houghton, Noel Llopis
http://tinyurl.com/gddtdd
#pracunittests
2004
@tenpn
#pracunittests
2004
@tenpn
Definitions
#pracunittests
Unit TestSingle explicit assumption
#pracunittests
Unit TestSingle explicit assumption
Integration Test
Many implicit assumptions
#pracunittests
Qualities of Good Unit Tests
#pracunittests
Qualities of Good Unit Tests
Readable
#pracunittests
Qualities of Good Unit Tests
Readable
Maintainable
#pracunittests
Qualities of Good Unit Tests
Readable
Maintainable
Trustworthy
Post
Mortem
#pracunittests
F1 2011 X360/PS3/PC
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
• 502 tests, 6700 lines of test code
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
• 502 tests, 6700 lines of test code
• 6200 lines of production code
#pracunittests
A Partial Succes
#pracunittests
• Clean, re-usable
code
A Partial Succes
#pracunittests
• Clean, re-usable
code
• Fewer bugs
A Partial Succes
#pracunittests
• Clean, re-usable
code
• Fewer bugs
• Easy to optimise
A Partial Succes
#pracunittests
• Clean, re-usable
code
• Fewer bugs
• Easy to optimise
• At end, treacle-like
progress
A Partial Succes
Unit Test
Anti-Patterns
#pracunittests
1/4: The Opaque Anti-Pattern
#pracunittests
// in LinearDescriptionFixture…
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f)
.withDirection(Direction.eLeft);
TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f);
}
1/4: The Opaque Anti-Pattern
#pracunittests
// in LinearDescriptionFixture…
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f)
.withDirection(Direction.eLeft);
TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f);
} wat
1/4: The Opaque Anti-Pattern
#pracunittests
Opaque: Hard to see HOW
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f)
.withDirection(Direction.eLeft);
TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f);
}
#pracunittests
Opaque: Hard to see HOW
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f)
.withDirection(Direction.eLeft);
TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f);
}
#pracunittests
Opaque: Hard to see HOW
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f)
.withDirection(Direction.eLeft);
TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f);
}
#pracunittests
Opaque: No Magic Literals
#pracunittests
Opaque: No Magic Literals
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
float someLeftOfOrigin = someOrigin - 1.0f;
!
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin).withDirection(Direction.eLeft);
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: No Magic Literals
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
float someLeftOfOrigin = someOrigin - 1.0f;
!
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin).withDirection(Direction.eLeft);
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: No Magic Literals
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
float someLeftOfOrigin = someOrigin - 1.0f;
!
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin).withDirection(Direction.eLeft);
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: No Magic Literals
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
float someLeftOfOrigin = someOrigin - 1.0f;
!
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin).withDirection(Direction.eLeft);
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
float someLeftOfOrigin = someOrigin - 1.0f;
!
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin).withDirection(Direction.eLeft);
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
float someLeftOfOrigin = someOrigin - 1.0f;
!
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin).withDirection(Direction.eLeft);
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: Informative,
Consistent Test Name
#pracunittests
Opaque: Informative,
Consistent Test Name
void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {
#pracunittests
Opaque: Informative,
Consistent Test Name
void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {
void testBackwardsToNormalLeftwardsGradient() {
#pracunittests
Opaque: Informative,
Consistent Test Name
void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {
void
void withDirection_Left_InvertsGradient() {
#pracunittests
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
float someLeftOfOrigin = someOrigin - 1.0f;
!
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin).withDirection(Direction.eLeft);
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
float someLeftOfOrigin = someOrigin - 1.0f;
!
LinearDescription leftDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin).withDirection(Direction.eLeft);
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: Arrange-Act-Assert
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
LinearDescription increasingDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin);
!
LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft);
!
float someLeftOfOrigin = someOrigin - 1.0f;
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
LinearDescription increasingDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin);
!
LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft);
!
float someLeftOfOrigin = someOrigin - 1.0f;
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
LinearDescription increasingDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin);
!
LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft);
!
float someLeftOfOrigin = someOrigin - 1.0f;
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
LinearDescription increasingDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin);
!
LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft);
!
float someLeftOfOrigin = someOrigin - 1.0f;
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float positiveGradient = 1.0f;
LinearDescription increasingDesc = DefaultFlatLinearDescription()
.withGradient(positiveGradient).withInitialValue(someValueAtOrigin)
.withOffsetOrigin(someOrigin);
!
LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft);
!
float someLeftOfOrigin = someOrigin - 1.0f;
float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin);
TEST_GREATER(valueToLeft, someValueAtOrigin);
}
#pracunittests
The Opaque Anti-Pattern
#pracunittests
The Opaque Anti-Pattern
• Hard to see "how"?
#pracunittests
The Opaque Anti-Pattern
• Hard to see "how"?
• Demystify magic literals
#pracunittests
The Opaque Anti-Pattern
• Hard to see "how"?
• Demystify magic literals
• Consistent informative test name
#pracunittests
The Opaque Anti-Pattern
• Hard to see "how"?
• Demystify magic literals
• Consistent informative test name
• Arrange-Act-Assert
#pracunittests
2/4: The Wet Anti-Pattern
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)
#pracunittests
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)
2/4: The Wet Anti-Pattern
RacingLineOffsets
#pracunittests
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)
2/4: The Wet Anti-Pattern
> Test library build failed with 235 error(s)
RacingLineOffsets
#pracunittests
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f;
!
RacingLineOffsets beyondOffsets = new RacingLineOffsets();
float leftRacingLineEdge =
-someRacingLineRadius - someOffsetBeyondRacingLine;
beyondOffsets.setSignedDistanceToRacingLine(
RacingLine.eLeftEdge, leftRacingLineEdge);
beyondOffsets.setSignedDistanceToRacingLine(
RacingLine.eCenter, -someOffsetBeyondRacingLine);
float rightRacingLineEdge =
someRacingLineRadius - someOffsetBeyondRacingLine;
beyondOffsets.setSignedDistanceToRacingLine(
RacingLine.eRightEdge, rightRacingLineEdge);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(beyondOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(idealRacingLineOffset, someRacingLineRadius);
}
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius = 10.0f;
float someOffsetWithinRadius = someRacingLineRadius * 0.8f;
!
RacingLineOffsets withinOffsets = new RacingLineOffsets();
float leftRacingLineEdge =
-someRacingLineRadius - someOffsetWithinRadius;
withinOfffsets.setSignedDistanceToRacingLine(
RacingLine.eLeftEdge, leftRacingLineEdge);
withinOffsets.setSignedDistanceToRacingLine(
RacingLine.eCenter, -someOffsetWithinRadius);
float rightRacingLineEdge =
someRacingLineRadius - someOffsetWithinRadius;
withinOffsets.setSignedDistanceToRacingLine(
RacingLine.eRightEdge, rightRacingLineEdge);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(withinOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(idealRacingLineOffset, someOffsetWithinRadius);
}
#pracunittests
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f;
!
RacingLineOffsets beyondOffsets = new RacingLineOffsets();
float leftRacingLineEdge =
-someRacingLineRadius - someOffsetBeyondRacingLine;
beyondOffsets.setSignedDistanceToRacingLine(
RacingLine.eLeftEdge, leftRacingLineEdge);
beyondOffsets.setSignedDistanceToRacingLine(
RacingLine.eCenter, -someOffsetBeyondRacingLine);
float rightRacingLineEdge =
someRacingLineRadius - someOffsetBeyondRacingLine;
beyondOffsets.setSignedDistanceToRacingLine(
RacingLine.eRightEdge, rightRacingLineEdge);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(beyondOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(idealRacingLineOffset, someRacingLineRadius);
}
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius = 10.0f;
float someOffsetWithinRadius = someRacingLineRadius * 0.8f;
!
RacingLineOffsets withinOffsets = new RacingLineOffsets();
float leftRacingLineEdge =
-someRacingLineRadius - someOffsetWithinRadius;
withinOfffsets.setSignedDistanceToRacingLine(
RacingLine.eLeftEdge, leftRacingLineEdge);
withinOffsets.setSignedDistanceToRacingLine(
RacingLine.eCenter, -someOffsetWithinRadius);
float rightRacingLineEdge =
someRacingLineRadius - someOffsetWithinRadius;
withinOffsets.setSignedDistanceToRacingLine(
RacingLine.eRightEdge, rightRacingLineEdge);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(withinOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(idealRacingLineOffset, someOffsetWithinRadius);
}
Not DRY
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius = 10.0f;
float someOffsetWithinRadius =
someRacingLineRadius * 0.8f;
!
RacingLineOffsets withinOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetWithinRadius);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(withinOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(
idealRacingLineOffset, someOffsetWithinRadius);
}
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffsetBeyondRacingLine =
someRacingLineRadius + 1.0f;
!
RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetBeyondRacingLine);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(beyondOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(
idealRacingLineOffset, withinOffsets.RightEdge);
}
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius = 10.0f;
float someOffsetWithinRadius =
someRacingLineRadius * 0.8f;
!
RacingLineOffsets withinOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetWithinRadius);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(withinOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(
idealRacingLineOffset, someOffsetWithinRadius);
}
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffsetBeyondRacingLine =
someRacingLineRadius + 1.0f;
!
RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetBeyondRacingLine);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(beyondOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(
idealRacingLineOffset, withinOffsets.RightEdge);
}
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius = 10.0f;
float someOffsetWithinRadius =
someRacingLineRadius * 0.8f;
!
RacingLineOffsets withinOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetWithinRadius);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(withinOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(
idealRacingLineOffset, someOffsetWithinRadius);
}
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffsetBeyondRacingLine =
someRacingLineRadius + 1.0f;
!
RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetBeyondRacingLine);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(beyondOffsets);
!
float idealRacingLineOffset = idealRequest.GetOffset();
TEST_EQUAL(
idealRacingLineOffset, withinOffsets.RightEdge);
}
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius = 10.0f;
float someOffsetWithinRadius =
someRacingLineRadius * 0.8f;
!
RacingLineOffsets withinOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetWithinRadius);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(withinOffsets);
!
test_tauOffsetEqual(idealRequest, someOffsetWithinRadius);
}
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffsetBeyondRacingLine =
someRacingLineRadius + 1.0f;
!
RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetBeyondRacingLine);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(beyondOffsets);
!
test_racingLineOffsetEqual(
idealRequest, withinOffsets.RightEdge);
}
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius = 10.0f;
float someOffsetWithinRadius =
someRacingLineRadius * 0.8f;
!
RacingLineOffsets withinOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetWithinRadius);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(withinOffsets);
!
test_tauOffsetEqual(idealRequest, someOffsetWithinRadius);
}
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffsetBeyondRacingLine =
someRacingLineRadius + 1.0f;
!
RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(
someRacingLineRadius, someOffsetBeyondRacingLine);
!
OffsetRequest idealRequest =
m_raceBehaviour.getIdealOffset(beyondOffsets);
!
test_racingLineOffsetEqual(
idealRequest, withinOffsets.RightEdge);
}
#pracunittests
The Wet Anti-Pattern
#pracunittests
The Wet Anti-Pattern
• Hard-to-maintain hacky tests?
#pracunittests
The Wet Anti-Pattern
• Hard-to-maintain hacky tests?
• Keep production sensibilities in unit test code
#pracunittests
The Wet Anti-Pattern
• Hard-to-maintain hacky tests?
• Keep production sensibilities in unit test code
• Stay DRY with helper functions and custom
asserts
#pracunittests
The Wet Anti-Pattern
• Hard-to-maintain hacky tests?
• Keep production sensibilities in unit test code
• Stay DRY with helper functions and custom
asserts
• Do not hide the call to the function under test
#pracunittests
3/4: The Deep Anti-Pattern
> Test failed:
> getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets
> With Assert: VehicleID 0 != 1
#pracunittests
void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() {
VehicleID someOwnerID = (VehicleID)1;
LinearDescription ownedDescription =
DefaultFlatLinearDescription().withOwner(someOwnerID);
!
float someNegativeOffset = -1.0f;
float ownerAtNegativeOffset =
ownedDescription.getOwnerAtOffset(someNegativeOffset);
float somePositiveOffset = 1.0f;
float ownerAtPositiveOffset =
ownedDescription.getOwnerAtOffset(somePositiveOffset);
!
test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID);
test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID);
}
#pracunittests
void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() {
VehicleID someOwnerID = (VehicleID)1;
LinearDescription ownedDescription =
DefaultFlatLinearDescription().withOwner(someOwnerID);
!
float someNegativeOffset = -1.0f;
float ownerAtNegativeOffset =
ownedDescription.getOwnerAtOffset(someNegativeOffset);
float somePositiveOffset = 1.0f;
float ownerAtPositiveOffset =
ownedDescription.getOwnerAtOffset(somePositiveOffset);
!
test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID);
test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID);
}
#pracunittests
void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() {
VehicleID someOwnerID = (VehicleID)1;
LinearDescription ownedDescription =
DefaultFlatLinearDescription().withOwner(someOwnerID);
!
float someNegativeOffset = -1.0f;
float ownerAtNegativeOffset =
ownedDescription.getOwnerAtOffset(someNegativeOffset);
float somePositiveOffset = 1.0f;
float ownerAtPositiveOffset =
ownedDescription.getOwnerAtOffset(somePositiveOffset);
!
test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID);
test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID);
}
#pracunittests
void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() {
VehicleID someOwnerID = (VehicleID)1;
LinearDescription ownedDescription =
DefaultFlatLinearDescription().withOwner(someOwnerID);
!
float someNegativeOffset = -1.0f;
float ownerAtNegativeOffset =
ownedDescription.getOwnerAtOffset(someNegativeOffset);
float somePositiveOffset = 1.0f;
float ownerAtPositiveOffset =
ownedDescription.getOwnerAtOffset(somePositiveOffset);
!
test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID);
test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID);
}
>1 explicit
assumption
#pracunittests
Deep: One Assert Per Test
void getOwnerAtOffset_WithOwnerAndNegativeOffset_ReturnsOwner() {
float someNegativeOffset = -1.0f;
!
float ownerAtNegativeOffset =
m_ownedDescription.getOwnerAtOffset(someNegativeOffset);
!
test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, m_someOwnerID);
}
!
void getOwnerAtOffset_WithOwnerAndPositiveOffset_ReturnsOwner() {
// *snip*
}
#pracunittests
> Test failed:
> getOwnerAtOffset_WithOwnerAndNegativeOffset_ReturnsOwner
> With Assert: VehicleID 0 != 1
> Test failed:
> getOwnerAtOffset_WithOwnerAndPositiveOffset_ReturnsOwner
> With Assert: VehicleID 0 != 1
#pracunittests
The Deep Anti-Pattern
#pracunittests
The Deep Anti-Pattern
• Test failures not fully informative?
#pracunittests
The Deep Anti-Pattern
• Test failures not fully informative?
• Too many explicit assumptions per test
#pracunittests
The Deep Anti-Pattern
• Test failures not fully informative?
• Too many explicit assumptions per test
• Minimise assumptions per test
#pracunittests
4/4: The Wide Anti-Pattern
#pracunittests
4/4: The Wide Anti-Pattern
> Executed 613 test(s), 599 test(s) passed, 14 test(s) failed.
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
!
BehaviourSystem behaviourSystem = new BehaviourSystem();
behaviourSystem.addBehaviour(draft);
!
behaviourSystem.updateWithBlackboard(worldInfo);
!
MovementRequest draftMovement = behaviourSystem.getMovementRequest();
TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset);
}
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
!
BehaviourSystem behaviourSystem = new BehaviourSystem();
behaviourSystem.addBehaviour(draft);
!
behaviourSystem.updateWithBlackboard(worldInfo);
!
MovementRequest draftMovement = behaviourSystem.getMovementRequest();
TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset);
}
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
!
BehaviourSystem behaviourSystem = new BehaviourSystem();
behaviourSystem.addBehaviour(draft);
!
behaviourSystem.updateWithBlackboard(worldInfo);
!
MovementRequest draftMovement = behaviourSystem.getMovementRequest();
TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset);
}
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
!
BehaviourSystem behaviourSystem = new BehaviourSystem();
behaviourSystem.addBehaviour(draft);
!
behaviourSystem.updateWithBlackboard(worldInfo);
!
MovementRequest draftMovement = behaviourSystem.getMovementRequest();
TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset);
}
>0 implicit
assumptions
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
!
BehaviourSystem behaviourSystem = new BehaviourSystem();
behaviourSystem.addBehaviour(draft);
!
behaviourSystem.updateWithBlackboard(worldInfo);
!
MovementRequest draftMovement = behaviourSystem.getMovementRequest();
TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset);
}
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
!
BehaviourSystem behaviourSystem = new BehaviourSystem();
behaviourSystem.addBehaviour(draft);
!
behaviourSystem.updateWithBlackboard(worldInfo);
!
MovementRequest draftMovement = behaviourSystem.getMovementRequest();
TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset);
}
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
!
BehaviourSystem behaviourSystem = new BehaviourSystem();
behaviourSystem.addBehaviour(draft);
!
behaviourSystem.updateWithBlackboard(worldInfo);
!
MovementRequest draftMovement = behaviourSystem.getMovementRequest();
TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset);
}
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
!
BehaviourSystem behaviourSystem = new BehaviourSystem();
behaviourSystem.addBehaviour(draft);
!
behaviourSystem.updateWithBlackboard(worldInfo);
!
MovementRequest draftMovement = behaviourSystem.getMovementRequest();
TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset);
}
#pracunittests
Wide: Seams
void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
Game Code
#pracunittests
Wide: Seams
void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
class HeatMap { virtual void WriteHeat(float offset, float value) { … } }
Game Code
#pracunittests
Wide: Seams
void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
class HeatMap { virtual void WriteHeat(float offset, float value) { … } }
class MockHeatMap : HeatMap { override void WriteHeat(float offset, float value) { … } }
Game Code
Test Library
#pracunittests
Wide: Seams
void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
class HeatMap { virtual void WriteHeat(float offset, float value) { … } }
class MockHeatMap : HeatMap { override void WriteHeat(float offset, float value) { … } }
Game Code
Test Library
#pracunittests
Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
MockHeatMap mockMap = new MockHeatMap();
!
draft.updateImpl(worldInfo, mockMap);
!
float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset();
TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset);
}
#pracunittests
Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
MockHeatMap mockMap = new MockHeatMap();
!
draft.updateImpl(worldInfo, mockMap);
!
float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset();
TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset);
}
#pracunittests
Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo();
float someDraftTargetVehicleOffset = 2.0f;
// *snip* blackboard setup
!
DraftBehaviour draft = new DraftBehaviour();
MockHeatMap mockMap = new MockHeatMap();
!
draft.updateImpl(worldInfo, mockMap);
!
float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset();
TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset);
}
#pracunittests
The Wide Anti-Pattern
#pracunittests
The Wide Anti-Pattern
• False-negative test failures?
#pracunittests
The Wide Anti-Pattern
• False-negative test failures?
• Many implicit assumptions
#pracunittests
The Wide Anti-Pattern
• False-negative test failures?
• Many implicit assumptions
• Isolate code with seams, to enable
simple fake impostors
#pracunittests
Recap
#pracunittests
Recap
• Respect unit test source code as much as
production source code
#pracunittests
Recap
• Respect unit test source code as much as
production source code
• Write once, read many
#pracunittests
Recap
• Respect unit test source code as much as
production source code
• Write once, read many
• Only 1 explicit assumption
#pracunittests
Recap
• Respect unit test source code as much as
production source code
• Write once, read many
• Only 1 explicit assumption
• Minimise implicit assumptions
#pracunittests
• andrew.fray@gmail.com
• @tenpn
• andrewfray.wordpress.com
• Roy Osherove: Art of Unit Testing
www.artofunittesting.com
• Michael Feathers: Working
Effectively with Legacy Code
• Steve Freeman & Nat Pryce:
Growing Object-Orientated
Software, Guided By Tests
Colour scheme by Miaka www.colourlovers.com/palette/444487/Curiosity_Killed

More Related Content

Similar to Practical unit testing GDC 2014

Anatomy of a Gem: Bane
Anatomy of a Gem: BaneAnatomy of a Gem: Bane
Anatomy of a Gem: BaneDaniel Wellman
 
Functions for nothing, and your tests for free
Functions for nothing, and your tests for freeFunctions for nothing, and your tests for free
Functions for nothing, and your tests for freeGeorge Pollard
 
O CPAN tem as ferramentas que você precisa para fazer TDD em Perl, o Coding D...
O CPAN tem as ferramentas que você precisa para fazer TDD em Perl, o Coding D...O CPAN tem as ferramentas que você precisa para fazer TDD em Perl, o Coding D...
O CPAN tem as ferramentas que você precisa para fazer TDD em Perl, o Coding D...Rodolfo Carvalho
 
When Tdd Goes Awry (IAD 2013)
When Tdd Goes Awry (IAD 2013)When Tdd Goes Awry (IAD 2013)
When Tdd Goes Awry (IAD 2013)Uberto Barbini
 
Test driven node.js
Test driven node.jsTest driven node.js
Test driven node.jsJay Harris
 
FizzBuzzではじめるテスト
FizzBuzzではじめるテストFizzBuzzではじめるテスト
FizzBuzzではじめるテストMasashi Shinbara
 
Unit Test Your Database
Unit Test Your DatabaseUnit Test Your Database
Unit Test Your DatabaseDavid Wheeler
 
DSR Testing (Part 1)
DSR Testing (Part 1)DSR Testing (Part 1)
DSR Testing (Part 1)Steve Upton
 
Testing, Performance Analysis, and jQuery 1.4
Testing, Performance Analysis, and jQuery 1.4Testing, Performance Analysis, and jQuery 1.4
Testing, Performance Analysis, and jQuery 1.4jeresig
 
STAMP Descartes Presentation
STAMP Descartes PresentationSTAMP Descartes Presentation
STAMP Descartes PresentationSTAMP Project
 
Swift testing ftw
Swift testing ftwSwift testing ftw
Swift testing ftwJorge Ortiz
 
How and Why Python is Used in the Model of Real-World Battlefield Scenarios
How and Why Python is Used in the Model of Real-World Battlefield ScenariosHow and Why Python is Used in the Model of Real-World Battlefield Scenarios
How and Why Python is Used in the Model of Real-World Battlefield ScenariosJoshua L. Davis
 
Understanding JavaScript Testing
Understanding JavaScript TestingUnderstanding JavaScript Testing
Understanding JavaScript Testingjeresig
 
Elixir/OTP for PHP developers
Elixir/OTP for PHP developersElixir/OTP for PHP developers
Elixir/OTP for PHP developersIgnacio Martín
 
PhpUnit Best Practices
PhpUnit Best PracticesPhpUnit Best Practices
PhpUnit Best PracticesEdorian
 
The $path to knowledge: What little it take to unit-test Perl.
The $path to knowledge: What little it take to unit-test Perl.The $path to knowledge: What little it take to unit-test Perl.
The $path to knowledge: What little it take to unit-test Perl.Workhorse Computing
 
More on Fitnesse and Continuous Integration (Silicon Valley code camp 2012)
More on Fitnesse and Continuous Integration (Silicon Valley code camp 2012)More on Fitnesse and Continuous Integration (Silicon Valley code camp 2012)
More on Fitnesse and Continuous Integration (Silicon Valley code camp 2012)Jen Wong
 
How to test complex SaaS applications - The family july 2014
How to test complex SaaS applications - The family july 2014How to test complex SaaS applications - The family july 2014
How to test complex SaaS applications - The family july 2014Guillaume POTIER
 

Similar to Practical unit testing GDC 2014 (20)

Anatomy of a Gem: Bane
Anatomy of a Gem: BaneAnatomy of a Gem: Bane
Anatomy of a Gem: Bane
 
Functions for nothing, and your tests for free
Functions for nothing, and your tests for freeFunctions for nothing, and your tests for free
Functions for nothing, and your tests for free
 
O CPAN tem as ferramentas que você precisa para fazer TDD em Perl, o Coding D...
O CPAN tem as ferramentas que você precisa para fazer TDD em Perl, o Coding D...O CPAN tem as ferramentas que você precisa para fazer TDD em Perl, o Coding D...
O CPAN tem as ferramentas que você precisa para fazer TDD em Perl, o Coding D...
 
When Tdd Goes Awry (IAD 2013)
When Tdd Goes Awry (IAD 2013)When Tdd Goes Awry (IAD 2013)
When Tdd Goes Awry (IAD 2013)
 
Testing smells
Testing smellsTesting smells
Testing smells
 
Test driven node.js
Test driven node.jsTest driven node.js
Test driven node.js
 
Hidden Gems of Ruby 1.9
Hidden Gems of Ruby 1.9Hidden Gems of Ruby 1.9
Hidden Gems of Ruby 1.9
 
FizzBuzzではじめるテスト
FizzBuzzではじめるテストFizzBuzzではじめるテスト
FizzBuzzではじめるテスト
 
Unit Test Your Database
Unit Test Your DatabaseUnit Test Your Database
Unit Test Your Database
 
DSR Testing (Part 1)
DSR Testing (Part 1)DSR Testing (Part 1)
DSR Testing (Part 1)
 
Testing, Performance Analysis, and jQuery 1.4
Testing, Performance Analysis, and jQuery 1.4Testing, Performance Analysis, and jQuery 1.4
Testing, Performance Analysis, and jQuery 1.4
 
STAMP Descartes Presentation
STAMP Descartes PresentationSTAMP Descartes Presentation
STAMP Descartes Presentation
 
Swift testing ftw
Swift testing ftwSwift testing ftw
Swift testing ftw
 
How and Why Python is Used in the Model of Real-World Battlefield Scenarios
How and Why Python is Used in the Model of Real-World Battlefield ScenariosHow and Why Python is Used in the Model of Real-World Battlefield Scenarios
How and Why Python is Used in the Model of Real-World Battlefield Scenarios
 
Understanding JavaScript Testing
Understanding JavaScript TestingUnderstanding JavaScript Testing
Understanding JavaScript Testing
 
Elixir/OTP for PHP developers
Elixir/OTP for PHP developersElixir/OTP for PHP developers
Elixir/OTP for PHP developers
 
PhpUnit Best Practices
PhpUnit Best PracticesPhpUnit Best Practices
PhpUnit Best Practices
 
The $path to knowledge: What little it take to unit-test Perl.
The $path to knowledge: What little it take to unit-test Perl.The $path to knowledge: What little it take to unit-test Perl.
The $path to knowledge: What little it take to unit-test Perl.
 
More on Fitnesse and Continuous Integration (Silicon Valley code camp 2012)
More on Fitnesse and Continuous Integration (Silicon Valley code camp 2012)More on Fitnesse and Continuous Integration (Silicon Valley code camp 2012)
More on Fitnesse and Continuous Integration (Silicon Valley code camp 2012)
 
How to test complex SaaS applications - The family july 2014
How to test complex SaaS applications - The family july 2014How to test complex SaaS applications - The family july 2014
How to test complex SaaS applications - The family july 2014
 

Recently uploaded

Why Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businessWhy Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businesspanagenda
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonAnna Loughnan Colquhoun
 
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbu
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu SubbuApidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbu
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbuapidays
 
Boost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfBoost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfsudhanshuwaghmare1
 
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...apidays
 
Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)wesley chun
 
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...apidays
 
AXA XL - Insurer Innovation Award Americas 2024
AXA XL - Insurer Innovation Award Americas 2024AXA XL - Insurer Innovation Award Americas 2024
AXA XL - Insurer Innovation Award Americas 2024The Digital Insurer
 
FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024The Digital Insurer
 
Strategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherStrategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherRemote DBA Services
 
Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...apidays
 
Ransomware_Q4_2023. The report. [EN].pdf
Ransomware_Q4_2023. The report. [EN].pdfRansomware_Q4_2023. The report. [EN].pdf
Ransomware_Q4_2023. The report. [EN].pdfOverkill Security
 
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...DianaGray10
 
DBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor PresentationDBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor PresentationDropbox
 
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...Jeffrey Haguewood
 
A Beginners Guide to Building a RAG App Using Open Source Milvus
A Beginners Guide to Building a RAG App Using Open Source MilvusA Beginners Guide to Building a RAG App Using Open Source Milvus
A Beginners Guide to Building a RAG App Using Open Source MilvusZilliz
 
Corporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptxCorporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptxRustici Software
 
2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...Martijn de Jong
 
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...Drew Madelung
 
TrustArc Webinar - Unlock the Power of AI-Driven Data Discovery
TrustArc Webinar - Unlock the Power of AI-Driven Data DiscoveryTrustArc Webinar - Unlock the Power of AI-Driven Data Discovery
TrustArc Webinar - Unlock the Power of AI-Driven Data DiscoveryTrustArc
 

Recently uploaded (20)

Why Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businessWhy Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire business
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt Robison
 
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbu
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu SubbuApidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbu
Apidays Singapore 2024 - Modernizing Securities Finance by Madhu Subbu
 
Boost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfBoost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdf
 
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
 
Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)
 
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
 
AXA XL - Insurer Innovation Award Americas 2024
AXA XL - Insurer Innovation Award Americas 2024AXA XL - Insurer Innovation Award Americas 2024
AXA XL - Insurer Innovation Award Americas 2024
 
FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024
 
Strategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherStrategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a Fresher
 
Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...
 
Ransomware_Q4_2023. The report. [EN].pdf
Ransomware_Q4_2023. The report. [EN].pdfRansomware_Q4_2023. The report. [EN].pdf
Ransomware_Q4_2023. The report. [EN].pdf
 
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
 
DBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor PresentationDBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor Presentation
 
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
 
A Beginners Guide to Building a RAG App Using Open Source Milvus
A Beginners Guide to Building a RAG App Using Open Source MilvusA Beginners Guide to Building a RAG App Using Open Source Milvus
A Beginners Guide to Building a RAG App Using Open Source Milvus
 
Corporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptxCorporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptx
 
2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...
 
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...
Strategies for Unlocking Knowledge Management in Microsoft 365 in the Copilot...
 
TrustArc Webinar - Unlock the Power of AI-Driven Data Discovery
TrustArc Webinar - Unlock the Power of AI-Driven Data DiscoveryTrustArc Webinar - Unlock the Power of AI-Driven Data Discovery
TrustArc Webinar - Unlock the Power of AI-Driven Data Discovery
 

Practical unit testing GDC 2014

  • 4. #pracunittests Backwards Is Forward: Making Better Games with Test-Driven Development http://gdcvault.com/play/1013416/Backwards-Is-Forward-Making-Better Sean Houghton, Noel Llopis http://tinyurl.com/gddtdd
  • 9. #pracunittests Unit TestSingle explicit assumption Integration Test Many implicit assumptions
  • 11. #pracunittests Qualities of Good Unit Tests Readable
  • 12. #pracunittests Qualities of Good Unit Tests Readable Maintainable
  • 13. #pracunittests Qualities of Good Unit Tests Readable Maintainable Trustworthy
  • 16. #pracunittests F1 2011 X360/PS3/PC • Isolated new subsystem
  • 17. #pracunittests F1 2011 X360/PS3/PC • Isolated new subsystem • 502 tests, 6700 lines of test code
  • 18. #pracunittests F1 2011 X360/PS3/PC • Isolated new subsystem • 502 tests, 6700 lines of test code • 6200 lines of production code
  • 21. #pracunittests • Clean, re-usable code • Fewer bugs A Partial Succes
  • 22. #pracunittests • Clean, re-usable code • Fewer bugs • Easy to optimise A Partial Succes
  • 23. #pracunittests • Clean, re-usable code • Fewer bugs • Easy to optimise • At end, treacle-like progress A Partial Succes
  • 26. #pracunittests // in LinearDescriptionFixture… void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); } 1/4: The Opaque Anti-Pattern
  • 27. #pracunittests // in LinearDescriptionFixture… void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); } wat 1/4: The Opaque Anti-Pattern
  • 28. #pracunittests Opaque: Hard to see HOW void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); }
  • 29. #pracunittests Opaque: Hard to see HOW void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); }
  • 30. #pracunittests Opaque: Hard to see HOW void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); }
  • 32. #pracunittests Opaque: No Magic Literals void testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; ! LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 33. #pracunittests Opaque: No Magic Literals void testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; ! LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 34. #pracunittests Opaque: No Magic Literals void testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; ! LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 35. #pracunittests Opaque: No Magic Literals void testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; ! LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 36. #pracunittests void testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; ! LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 37. #pracunittests void testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; ! LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 39. #pracunittests Opaque: Informative, Consistent Test Name void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {
  • 40. #pracunittests Opaque: Informative, Consistent Test Name void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() { void testBackwardsToNormalLeftwardsGradient() {
  • 41. #pracunittests Opaque: Informative, Consistent Test Name void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() { void void withDirection_Left_InvertsGradient() {
  • 42. #pracunittests void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; ! LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 43. #pracunittests void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; ! LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 45. #pracunittests Opaque: Arrange-Act-Assert void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); ! LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); ! float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 46. #pracunittests Opaque: Arrange-Act-Assert void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); ! LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); ! float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 47. #pracunittests Opaque: Arrange-Act-Assert void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); ! LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); ! float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 48. #pracunittests Opaque: Arrange-Act-Assert void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); ! LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); ! float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 49. #pracunittests Opaque: Arrange-Act-Assert void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); ! LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); ! float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }
  • 52. #pracunittests The Opaque Anti-Pattern • Hard to see "how"? • Demystify magic literals
  • 53. #pracunittests The Opaque Anti-Pattern • Hard to see "how"? • Demystify magic literals • Consistent informative test name
  • 54. #pracunittests The Opaque Anti-Pattern • Hard to see "how"? • Demystify magic literals • Consistent informative test name • Arrange-Act-Assert
  • 55. #pracunittests 2/4: The Wet Anti-Pattern RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)
  • 57. #pracunittests RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int) 2/4: The Wet Anti-Pattern > Test library build failed with 235 error(s) RacingLineOffsets
  • 58. #pracunittests void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() { float someRacingLineRadius = 10.0f; float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f; ! RacingLineOffsets beyondOffsets = new RacingLineOffsets(); float leftRacingLineEdge = -someRacingLineRadius - someOffsetBeyondRacingLine; beyondOffsets.setSignedDistanceToRacingLine( RacingLine.eLeftEdge, leftRacingLineEdge); beyondOffsets.setSignedDistanceToRacingLine( RacingLine.eCenter, -someOffsetBeyondRacingLine); float rightRacingLineEdge = someRacingLineRadius - someOffsetBeyondRacingLine; beyondOffsets.setSignedDistanceToRacingLine( RacingLine.eRightEdge, rightRacingLineEdge); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(beyondOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL(idealRacingLineOffset, someRacingLineRadius); } void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = new RacingLineOffsets(); float leftRacingLineEdge = -someRacingLineRadius - someOffsetWithinRadius; withinOfffsets.setSignedDistanceToRacingLine( RacingLine.eLeftEdge, leftRacingLineEdge); withinOffsets.setSignedDistanceToRacingLine( RacingLine.eCenter, -someOffsetWithinRadius); float rightRacingLineEdge = someRacingLineRadius - someOffsetWithinRadius; withinOffsets.setSignedDistanceToRacingLine( RacingLine.eRightEdge, rightRacingLineEdge); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL(idealRacingLineOffset, someOffsetWithinRadius); }
  • 59. #pracunittests void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() { float someRacingLineRadius = 10.0f; float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f; ! RacingLineOffsets beyondOffsets = new RacingLineOffsets(); float leftRacingLineEdge = -someRacingLineRadius - someOffsetBeyondRacingLine; beyondOffsets.setSignedDistanceToRacingLine( RacingLine.eLeftEdge, leftRacingLineEdge); beyondOffsets.setSignedDistanceToRacingLine( RacingLine.eCenter, -someOffsetBeyondRacingLine); float rightRacingLineEdge = someRacingLineRadius - someOffsetBeyondRacingLine; beyondOffsets.setSignedDistanceToRacingLine( RacingLine.eRightEdge, rightRacingLineEdge); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(beyondOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL(idealRacingLineOffset, someRacingLineRadius); } void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = new RacingLineOffsets(); float leftRacingLineEdge = -someRacingLineRadius - someOffsetWithinRadius; withinOfffsets.setSignedDistanceToRacingLine( RacingLine.eLeftEdge, leftRacingLineEdge); withinOffsets.setSignedDistanceToRacingLine( RacingLine.eCenter, -someOffsetWithinRadius); float rightRacingLineEdge = someRacingLineRadius - someOffsetWithinRadius; withinOffsets.setSignedDistanceToRacingLine( RacingLine.eRightEdge, rightRacingLineEdge); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL(idealRacingLineOffset, someOffsetWithinRadius); } Not DRY
  • 60. #pracunittests Wet: Helper Functions void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetWithinRadius); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL( idealRacingLineOffset, someOffsetWithinRadius); } void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() { float someRacingLineRadius = 10.0f; float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f; ! RacingLineOffsets beyondOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetBeyondRacingLine); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(beyondOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL( idealRacingLineOffset, withinOffsets.RightEdge); }
  • 61. #pracunittests Wet: Helper Functions void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetWithinRadius); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL( idealRacingLineOffset, someOffsetWithinRadius); } void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() { float someRacingLineRadius = 10.0f; float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f; ! RacingLineOffsets beyondOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetBeyondRacingLine); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(beyondOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL( idealRacingLineOffset, withinOffsets.RightEdge); }
  • 62. #pracunittests Wet: Helper Functions void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetWithinRadius); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL( idealRacingLineOffset, someOffsetWithinRadius); } void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() { float someRacingLineRadius = 10.0f; float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f; ! RacingLineOffsets beyondOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetBeyondRacingLine); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(beyondOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL( idealRacingLineOffset, withinOffsets.RightEdge); }
  • 63. #pracunittests Wet: Helper Functions void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetWithinRadius); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! test_tauOffsetEqual(idealRequest, someOffsetWithinRadius); } void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() { float someRacingLineRadius = 10.0f; float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f; ! RacingLineOffsets beyondOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetBeyondRacingLine); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(beyondOffsets); ! test_racingLineOffsetEqual( idealRequest, withinOffsets.RightEdge); }
  • 64. #pracunittests Wet: Helper Functions void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetWithinRadius); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! test_tauOffsetEqual(idealRequest, someOffsetWithinRadius); } void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() { float someRacingLineRadius = 10.0f; float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f; ! RacingLineOffsets beyondOffsets = CreateRacingLineOffsets( someRacingLineRadius, someOffsetBeyondRacingLine); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(beyondOffsets); ! test_racingLineOffsetEqual( idealRequest, withinOffsets.RightEdge); }
  • 66. #pracunittests The Wet Anti-Pattern • Hard-to-maintain hacky tests?
  • 67. #pracunittests The Wet Anti-Pattern • Hard-to-maintain hacky tests? • Keep production sensibilities in unit test code
  • 68. #pracunittests The Wet Anti-Pattern • Hard-to-maintain hacky tests? • Keep production sensibilities in unit test code • Stay DRY with helper functions and custom asserts
  • 69. #pracunittests The Wet Anti-Pattern • Hard-to-maintain hacky tests? • Keep production sensibilities in unit test code • Stay DRY with helper functions and custom asserts • Do not hide the call to the function under test
  • 70. #pracunittests 3/4: The Deep Anti-Pattern > Test failed: > getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets > With Assert: VehicleID 0 != 1
  • 71. #pracunittests void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleID someOwnerID = (VehicleID)1; LinearDescription ownedDescription = DefaultFlatLinearDescription().withOwner(someOwnerID); ! float someNegativeOffset = -1.0f; float ownerAtNegativeOffset = ownedDescription.getOwnerAtOffset(someNegativeOffset); float somePositiveOffset = 1.0f; float ownerAtPositiveOffset = ownedDescription.getOwnerAtOffset(somePositiveOffset); ! test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID); test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID); }
  • 72. #pracunittests void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleID someOwnerID = (VehicleID)1; LinearDescription ownedDescription = DefaultFlatLinearDescription().withOwner(someOwnerID); ! float someNegativeOffset = -1.0f; float ownerAtNegativeOffset = ownedDescription.getOwnerAtOffset(someNegativeOffset); float somePositiveOffset = 1.0f; float ownerAtPositiveOffset = ownedDescription.getOwnerAtOffset(somePositiveOffset); ! test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID); test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID); }
  • 73. #pracunittests void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleID someOwnerID = (VehicleID)1; LinearDescription ownedDescription = DefaultFlatLinearDescription().withOwner(someOwnerID); ! float someNegativeOffset = -1.0f; float ownerAtNegativeOffset = ownedDescription.getOwnerAtOffset(someNegativeOffset); float somePositiveOffset = 1.0f; float ownerAtPositiveOffset = ownedDescription.getOwnerAtOffset(somePositiveOffset); ! test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID); test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID); }
  • 74. #pracunittests void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleID someOwnerID = (VehicleID)1; LinearDescription ownedDescription = DefaultFlatLinearDescription().withOwner(someOwnerID); ! float someNegativeOffset = -1.0f; float ownerAtNegativeOffset = ownedDescription.getOwnerAtOffset(someNegativeOffset); float somePositiveOffset = 1.0f; float ownerAtPositiveOffset = ownedDescription.getOwnerAtOffset(somePositiveOffset); ! test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID); test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID); } >1 explicit assumption
  • 75. #pracunittests Deep: One Assert Per Test void getOwnerAtOffset_WithOwnerAndNegativeOffset_ReturnsOwner() { float someNegativeOffset = -1.0f; ! float ownerAtNegativeOffset = m_ownedDescription.getOwnerAtOffset(someNegativeOffset); ! test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, m_someOwnerID); } ! void getOwnerAtOffset_WithOwnerAndPositiveOffset_ReturnsOwner() { // *snip* }
  • 76. #pracunittests > Test failed: > getOwnerAtOffset_WithOwnerAndNegativeOffset_ReturnsOwner > With Assert: VehicleID 0 != 1 > Test failed: > getOwnerAtOffset_WithOwnerAndPositiveOffset_ReturnsOwner > With Assert: VehicleID 0 != 1
  • 78. #pracunittests The Deep Anti-Pattern • Test failures not fully informative?
  • 79. #pracunittests The Deep Anti-Pattern • Test failures not fully informative? • Too many explicit assumptions per test
  • 80. #pracunittests The Deep Anti-Pattern • Test failures not fully informative? • Too many explicit assumptions per test • Minimise assumptions per test
  • 82. #pracunittests 4/4: The Wide Anti-Pattern > Executed 613 test(s), 599 test(s) passed, 14 test(s) failed.
  • 83. #pracunittests // in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); ! BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); ! behaviourSystem.updateWithBlackboard(worldInfo); ! MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }
  • 84. #pracunittests // in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); ! BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); ! behaviourSystem.updateWithBlackboard(worldInfo); ! MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }
  • 85. #pracunittests // in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); ! BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); ! behaviourSystem.updateWithBlackboard(worldInfo); ! MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }
  • 86. #pracunittests // in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); ! BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); ! behaviourSystem.updateWithBlackboard(worldInfo); ! MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); } >0 implicit assumptions
  • 87. #pracunittests // in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); ! BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); ! behaviourSystem.updateWithBlackboard(worldInfo); ! MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }
  • 88. #pracunittests // in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); ! BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); ! behaviourSystem.updateWithBlackboard(worldInfo); ! MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }
  • 89. #pracunittests // in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); ! BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); ! behaviourSystem.updateWithBlackboard(worldInfo); ! MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }
  • 90. #pracunittests // in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); ! BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); ! behaviourSystem.updateWithBlackboard(worldInfo); ! MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }
  • 92. #pracunittests Wide: Seams void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut); class HeatMap { virtual void WriteHeat(float offset, float value) { … } } Game Code
  • 93. #pracunittests Wide: Seams void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut); class HeatMap { virtual void WriteHeat(float offset, float value) { … } } class MockHeatMap : HeatMap { override void WriteHeat(float offset, float value) { … } } Game Code Test Library
  • 94. #pracunittests Wide: Seams void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut); class HeatMap { virtual void WriteHeat(float offset, float value) { … } } class MockHeatMap : HeatMap { override void WriteHeat(float offset, float value) { … } } Game Code Test Library
  • 95. #pracunittests Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); MockHeatMap mockMap = new MockHeatMap(); ! draft.updateImpl(worldInfo, mockMap); ! float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset(); TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset); }
  • 96. #pracunittests Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); MockHeatMap mockMap = new MockHeatMap(); ! draft.updateImpl(worldInfo, mockMap); ! float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset(); TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset); }
  • 97. #pracunittests Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup ! DraftBehaviour draft = new DraftBehaviour(); MockHeatMap mockMap = new MockHeatMap(); ! draft.updateImpl(worldInfo, mockMap); ! float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset(); TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset); }
  • 99. #pracunittests The Wide Anti-Pattern • False-negative test failures?
  • 100. #pracunittests The Wide Anti-Pattern • False-negative test failures? • Many implicit assumptions
  • 101. #pracunittests The Wide Anti-Pattern • False-negative test failures? • Many implicit assumptions • Isolate code with seams, to enable simple fake impostors
  • 103. #pracunittests Recap • Respect unit test source code as much as production source code
  • 104. #pracunittests Recap • Respect unit test source code as much as production source code • Write once, read many
  • 105. #pracunittests Recap • Respect unit test source code as much as production source code • Write once, read many • Only 1 explicit assumption
  • 106. #pracunittests Recap • Respect unit test source code as much as production source code • Write once, read many • Only 1 explicit assumption • Minimise implicit assumptions
  • 107. #pracunittests • andrew.fray@gmail.com • @tenpn • andrewfray.wordpress.com • Roy Osherove: Art of Unit Testing www.artofunittesting.com • Michael Feathers: Working Effectively with Legacy Code • Steve Freeman & Nat Pryce: Growing Object-Orientated Software, Guided By Tests Colour scheme by Miaka www.colourlovers.com/palette/444487/Curiosity_Killed