http://tinyurl.com/sf-rnt
UnitTests
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 TestMany 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
MaintainableTrustworthy
PostMortem
#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 TestAnti-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 Literalsvoid 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 Literalsvoid 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 Literalsvoid 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 Literalsvoid 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-Assertvoid 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-Assertvoid 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-Assertvoid 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-Assertvoid 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-Assertvoid 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 Functionsvoid 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 Functionsvoid 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 Functionsvoid 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 Functionsvoid 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 Functionsvoid 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 Testvoid 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: Seamsvoid DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
Game Code
#pracunittests
Wide: Seamsvoid DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
class HeatMap { virtual void WriteHeat(float offset, float value) { … } }
Game Code
#pracunittests
Wide: Seamsvoid 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: Seamsvoid 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
• @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