Upload
andrew-fray
View
51
Download
2
Tags:
Embed Size (px)
DESCRIPTION
Citation preview
http://tinyurl.com/sf-rnt
http://spryfox.com/our-games/road-not-taken/ shut down wifi, skype. RECORD!
UnitTests
Practical
Andrew Fray, Spry Fox
Thank you for coming. This is Practical Unit Tests, and I’m Andrew Fray. My talk today is about how to structure your unit tests to aid iteration. We’re going to be looking in detail at my unit tests from a shipped AAA console project, the mistakes I made in writing them, and the concrete costs they caused. Then we’ll look at simple ways to avoid those costs in your own unit tests.!definitions Post mortem Anti-patterns
#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
Unit TestSingle explicit assumption
Integration TestMany implicit assumptions
IMPORTANT functional/integration interchangable
#pracunittests
Qualities of Good Unit Tests
Readable - why. up to speed. Maintainable - how. modify for iterating. Trustworthy - quick, deterministic. no external resources.
#pracunittests
Qualities of Good Unit Tests
Readable
Readable - why. up to speed. Maintainable - how. modify for iterating. Trustworthy - quick, deterministic. no external resources.
#pracunittests
Qualities of Good Unit Tests
Readable
Maintainable
Readable - why. up to speed. Maintainable - how. modify for iterating. Trustworthy - quick, deterministic. no external resources.
#pracunittests
Qualities of Good Unit Tests
Readable
MaintainableTrustworthy
Readable - why. up to speed. Maintainable - how. modify for iterating. Trustworthy - quick, deterministic. no external resources.
#pracunittests
F1 2011 X360/PS3/PC
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
• 502 tests, 6700 lines of test code
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
• 502 tests, 6700 lines of test code
• 6200 lines of production code
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
A Partial Succes
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
• Clean, re-usable code
A Partial Succes
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
• Clean, re-usable code
• Fewer bugs
A Partial Succes
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
• Clean, re-usable code
• Fewer bugs
• Easy to optimise
A Partial Succes
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
• Clean, re-usable code
• Fewer bugs
• Easy to optimise
• At end, treacle-like progress
A Partial Succes
PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously
#pracunittests
1/4: The Opaque Anti-Pattern
types: light grey names: bold dark grey literals: red else: dark grey
#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
types: light grey names: bold dark grey literals: red else: dark grey
#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
types: light grey names: bold dark grey literals: red else: dark grey
#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); }
types: light grey names: bold dark grey literals: red else: dark grey
#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); }
types: light grey names: bold dark grey literals: red else: dark grey
#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); }
types: light grey names: bold dark grey literals: red else: dark grey
#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); }
break the rules when you need to!
#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); }
break the rules when you need to!
#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); }
break the rules when you need to!
#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); }
break the rules when you need to!
#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
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-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• Hard to see "how"?
• Demystify magic literals
breathe at end!
#pracunittests
The Opaque Anti-Pattern• Hard to see "how"?
• Demystify magic literals
• Consistent informative test name
breathe at end!
#pracunittests
The Opaque Anti-Pattern• Hard to see "how"?
• Demystify magic literals
• Consistent informative test name
• Arrange-Act-Assert
breathe at end!
#pracunittests
2/4: The Wet Anti-Pattern
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)
On refactoring
#pracunittests
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)
2/4: The Wet Anti-Pattern
RacingLineOffsets
On refactoring
#pracunittests
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)
2/4: The Wet Anti-Pattern
> Test library build failed with 235 error(s)
RacingLineOffsets
On refactoring
#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• Hard-to-maintain hacky tests?
• Keep production sensibilities in unit test code
breathe at end!
#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
breathe at end!
#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
breathe at end!
#pracunittests
3/4: The Deep Anti-Pattern
> Test failed: > getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets > With Assert: VehicleID 0 != 1
on introducing a bug chi == “kai”!
#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); }
make it clear
#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); }
make it clear
#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); }
make it clear
#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
make it clear
#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
• Test failures not fully informative?
• Too many explicit assumptions per test
breathe at end!
#pracunittests
The Deep Anti-Pattern
• Test failures not fully informative?
• Too many explicit assumptions per test
• Minimise assumptions per test
breathe at end!
#pracunittests
4/4: The Wide Anti-Pattern
> Executed 613 test(s), 599 test(s) passed, 14 test(s) failed.
On introducing a bug in behaviour system
#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
Inversion of Control Interfaces/virtual functions Mocking frameworks/helper classes
#pracunittests
Wide: Seamsvoid DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
class HeatMap { virtual void WriteHeat(float offset, float value) { … } }
Game Code
Inversion of Control Interfaces/virtual functions Mocking frameworks/helper classes
#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
Inversion of Control Interfaces/virtual functions Mocking frameworks/helper classes
#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
Inversion of Control Interfaces/virtual functions Mocking frameworks/helper classes
#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• False-negative test failures?
• Many implicit assumptions
breathe at end!
#pracunittests
The Wide Anti-Pattern• False-negative test failures?
• Many implicit assumptions
• Isolate code with seams, to enable simple fake impostors
breathe at end!
#pracunittests
Recap
Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions
#pracunittests
Recap• Respect unit test source code as much as
production source code
Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions
#pracunittests
Recap• Respect unit test source code as much as
production source code
• Write once, read many
Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions
#pracunittests
Recap• Respect unit test source code as much as
production source code
• Write once, read many
• Only 1 explicit assumption
Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions
#pracunittests
Recap• Respect unit test source code as much as
production source code
• Write once, read many
• Only 1 explicit assumption
• Minimise implicit assumptions
Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible 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
Feedback! Thank the CAs!