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

More Related Content

Similar to Practical unit testing GDC 2014

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

Similar to Practical unit testing GDC 2014 (20)

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

Recently uploaded

Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 3652toLead Limited
 
Install Stable Diffusion in windows machine
Install Stable Diffusion in windows machineInstall Stable Diffusion in windows machine
Install Stable Diffusion in windows machinePadma Pradeep
 
Benefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksBenefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksSoftradix Technologies
 
SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024Scott Keck-Warren
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationRidwan Fadjar
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhisoniya singh
 
How to convert PDF to text with Nanonets
How to convert PDF to text with NanonetsHow to convert PDF to text with Nanonets
How to convert PDF to text with Nanonetsnaman860154
 
Unleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubUnleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubKalema Edgar
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Patryk Bandurski
 
Transcript: New from BookNet Canada for 2024: BNC BiblioShare - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC BiblioShare - Tech Forum 2024Transcript: New from BookNet Canada for 2024: BNC BiblioShare - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC BiblioShare - Tech Forum 2024BookNet Canada
 
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticsKotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticscarlostorres15106
 
Unlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsUnlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsPrecisely
 
Presentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreterPresentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreternaman860154
 
Streamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupStreamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupFlorian Wilhelm
 
Unblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesUnblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesSinan KOZAK
 
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptxMaking_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptxnull - The Open Security Community
 
08448380779 Call Girls In Diplomatic Enclave Women Seeking Men
08448380779 Call Girls In Diplomatic Enclave Women Seeking Men08448380779 Call Girls In Diplomatic Enclave Women Seeking Men
08448380779 Call Girls In Diplomatic Enclave Women Seeking MenDelhi Call girls
 
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...shyamraj55
 

Recently uploaded (20)

Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
 
Install Stable Diffusion in windows machine
Install Stable Diffusion in windows machineInstall Stable Diffusion in windows machine
Install Stable Diffusion in windows machine
 
Benefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksBenefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other Frameworks
 
SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 Presentation
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
 
How to convert PDF to text with Nanonets
How to convert PDF to text with NanonetsHow to convert PDF to text with Nanonets
How to convert PDF to text with Nanonets
 
Unleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubUnleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding Club
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
 
Transcript: New from BookNet Canada for 2024: BNC BiblioShare - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC BiblioShare - Tech Forum 2024Transcript: New from BookNet Canada for 2024: BNC BiblioShare - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC BiblioShare - Tech Forum 2024
 
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmaticsKotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
 
Unlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power SystemsUnlocking the Potential of the Cloud for IBM Power Systems
Unlocking the Potential of the Cloud for IBM Power Systems
 
Presentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreterPresentation on how to chat with PDF using ChatGPT code interpreter
Presentation on how to chat with PDF using ChatGPT code interpreter
 
Streamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupStreamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project Setup
 
Unblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesUnblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen Frames
 
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptxMaking_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
Making_way_through_DLL_hollowing_inspite_of_CFG_by_Debjeet Banerjee.pptx
 
08448380779 Call Girls In Diplomatic Enclave Women Seeking Men
08448380779 Call Girls In Diplomatic Enclave Women Seeking Men08448380779 Call Girls In Diplomatic Enclave Women Seeking Men
08448380779 Call Girls In Diplomatic Enclave Women Seeking Men
 
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptxE-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
 
The transition to renewables in India.pdf
The transition to renewables in India.pdfThe transition to renewables in India.pdf
The transition to renewables in India.pdf
 
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
 

Practical unit testing GDC 2014

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