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

Practical unit testing GDC 2014

  • 1.
  • 2.
  • 3.
  • 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
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
    #pracunittests Unit TestSingle explicitassumption Integration Test Many implicit assumptions
  • 10.
  • 11.
  • 12.
    #pracunittests Qualities of GoodUnit Tests Readable Maintainable
  • 13.
    #pracunittests Qualities of GoodUnit Tests Readable Maintainable Trustworthy
  • 14.
  • 15.
  • 16.
  • 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
  • 19.
  • 20.
  • 21.
  • 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
  • 24.
  • 25.
  • 26.
    #pracunittests // in LinearDescriptionFixture… voidtestBackwardsToNormalLeftwardsGradient() { 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… voidtestBackwardsToNormalLeftwardsGradient() { 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 tosee 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 tosee 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 tosee 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); }
  • 31.
  • 32.
    #pracunittests Opaque: No MagicLiterals 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 MagicLiterals 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 MagicLiterals 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 MagicLiterals 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() { floatsomeValueAtOrigin = 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() { floatsomeValueAtOrigin = 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); }
  • 38.
  • 39.
    #pracunittests Opaque: Informative, Consistent TestName void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {
  • 40.
    #pracunittests Opaque: Informative, Consistent TestName void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() { void testBackwardsToNormalLeftwardsGradient() {
  • 41.
    #pracunittests Opaque: Informative, Consistent TestName void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() { void void withDirection_Left_InvertsGradient() {
  • 42.
    #pracunittests void withDirection_Left_InvertsGradient() { floatsomeValueAtOrigin = 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() { floatsomeValueAtOrigin = 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); }
  • 44.
  • 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); }
  • 50.
  • 51.
  • 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 WetAnti-Pattern RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)
  • 56.
  • 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() { floatsomeRacingLineRadius = 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() { floatsomeRacingLineRadius = 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 voidgetIdealOffset_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 voidgetIdealOffset_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 voidgetIdealOffset_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 voidgetIdealOffset_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 voidgetIdealOffset_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); }
  • 65.
  • 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 DeepAnti-Pattern > Test failed: > getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets > With Assert: VehicleID 0 != 1
  • 71.
    #pracunittests void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleIDsomeOwnerID = (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() { VehicleIDsomeOwnerID = (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() { VehicleIDsomeOwnerID = (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() { VehicleIDsomeOwnerID = (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 AssertPer 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
  • 77.
  • 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
  • 81.
  • 82.
    #pracunittests 4/4: The WideAnti-Pattern > Executed 613 test(s), 599 test(s) passed, 14 test(s) failed.
  • 83.
    #pracunittests // in DraftBehaviourFixture… voidupdateImpl_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… voidupdateImpl_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… voidupdateImpl_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… voidupdateImpl_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… voidupdateImpl_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… voidupdateImpl_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… voidupdateImpl_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… voidupdateImpl_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); }
  • 91.
  • 92.
    #pracunittests Wide: Seams void DraftBehaviour.updateImpl(WorldInfowi, HeatMap heatInOut); class HeatMap { virtual void WriteHeat(float offset, float value) { … } } Game Code
  • 93.
    #pracunittests Wide: Seams void DraftBehaviour.updateImpl(WorldInfowi, 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(WorldInfowi, 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); }
  • 98.
  • 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
  • 102.
  • 103.
    #pracunittests Recap • Respect unittest source code as much as production source code
  • 104.
    #pracunittests Recap • Respect unittest source code as much as production source code • Write once, read many
  • 105.
    #pracunittests Recap • Respect unittest source code as much as production source code • Write once, read many • Only 1 explicit assumption
  • 106.
    #pracunittests Recap • Respect unittest 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