Practical unit testing GDC 2014

Preview:

DESCRIPTION

Slides from my GDC 2014 talk on practical unit testing. How can bad unit tests slow iteration? And how can you fix them?

Citation preview

http://tinyurl.com/sf-rnt

UnitTests

Practical

Andrew Fray, Spry Fox

#pracunittests

Test Driven Development

#pracunittests

Backwards Is Forward: Making Better Games with Test-Driven Development

http://gdcvault.com/play/1013416/Backwards-Is-Forward-Making-Better

Sean Houghton, Noel Llopis

http://tinyurl.com/gddtdd

#pracunittests

2004

@tenpn

#pracunittests

2004

@tenpn

Definitions

#pracunittests

Unit TestSingle explicit assumption

#pracunittests

Unit TestSingle explicit assumption

Integration TestMany implicit assumptions

#pracunittests

Qualities of Good Unit Tests

#pracunittests

Qualities of Good Unit Tests

Readable

#pracunittests

Qualities of Good Unit Tests

Readable

Maintainable

#pracunittests

Qualities of Good Unit Tests

Readable

MaintainableTrustworthy

PostMortem

#pracunittests

F1 2011 X360/PS3/PC

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

• 502 tests, 6700 lines of test code

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

• 502 tests, 6700 lines of test code

• 6200 lines of production code

#pracunittests

A Partial Succes

#pracunittests

• Clean, re-usable code

A Partial Succes

#pracunittests

• Clean, re-usable code

• Fewer bugs

A Partial Succes

#pracunittests

• Clean, re-usable code

• Fewer bugs

• Easy to optimise

A Partial Succes

#pracunittests

• Clean, re-usable code

• Fewer bugs

• Easy to optimise

• At end, treacle-like progress

A Partial Succes

Unit TestAnti-Patterns

#pracunittests

1/4: The Opaque Anti-Pattern

#pracunittests

// in LinearDescriptionFixture… void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); }

1/4: The Opaque Anti-Pattern

#pracunittests

// in LinearDescriptionFixture… void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); } wat

1/4: The Opaque Anti-Pattern

#pracunittests

Opaque: Hard to see HOW

void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); }

#pracunittests

Opaque: Hard to see HOW

void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); }

#pracunittests

Opaque: Hard to see HOW

void testBackwardsToNormalLeftwardsGradient() { LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(1f).withInitialValue(10.0f).withOffsetOrigin(0.0f) .withDirection(Direction.eLeft); TEST_GREATER(leftDesc.getValueAtOffset(-1.0f), 10.0f); }

#pracunittests

Opaque: No Magic Literals

#pracunittests

Opaque: No Magic Literalsvoid testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; !

LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: No Magic Literalsvoid testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; !

LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: No Magic Literalsvoid testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; !

LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: No Magic Literalsvoid testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; !

LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

void testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; !

LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

void testBackwardsToNormalLeftwardsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; !

LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: Informative, Consistent Test Name

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

void testBackwardsToNormalLeftwardsGradient() {

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

void

void withDirection_Left_InvertsGradient() {

#pracunittests

void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; !

LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

void withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; float someLeftOfOrigin = someOrigin - 1.0f; !

LinearDescription leftDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin).withDirection(Direction.eLeft); float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: Arrange-Act-Assert

#pracunittests

Opaque: Arrange-Act-Assertvoid withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); !

LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); !

float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: Arrange-Act-Assertvoid withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); !

LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); !

float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: Arrange-Act-Assertvoid withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); !

LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); !

float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: Arrange-Act-Assertvoid withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); !

LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); !

float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

Opaque: Arrange-Act-Assertvoid withDirection_Left_InvertsGradient() { float someValueAtOrigin = 10.0f; float someOrigin = 0.0f; float positiveGradient = 1.0f; LinearDescription increasingDesc = DefaultFlatLinearDescription() .withGradient(positiveGradient).withInitialValue(someValueAtOrigin) .withOffsetOrigin(someOrigin); !

LinearDescription leftDesc = increasingDesc.withDirection(Direction.eLeft); !

float someLeftOfOrigin = someOrigin - 1.0f; float valueToLeft = leftDesc.getValueAtOffset(someLeftOfOrigin); TEST_GREATER(valueToLeft, someValueAtOrigin); }

#pracunittests

The Opaque Anti-Pattern

#pracunittests

The Opaque Anti-Pattern• Hard to see "how"?

#pracunittests

The Opaque Anti-Pattern• Hard to see "how"?

• Demystify magic literals

#pracunittests

The Opaque Anti-Pattern• Hard to see "how"?

• Demystify magic literals

• Consistent informative test name

#pracunittests

The Opaque Anti-Pattern• Hard to see "how"?

• Demystify magic literals

• Consistent informative test name

• Arrange-Act-Assert

#pracunittests

2/4: The Wet Anti-Pattern

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)

#pracunittests

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)

2/4: The Wet Anti-Pattern

RacingLineOffsets

#pracunittests

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)

2/4: The Wet Anti-Pattern

> Test library build failed with 235 error(s)

RacingLineOffsets

#pracunittests

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f;

! RacingLineOffsets beyondOffsets = new RacingLineOffsets();

float leftRacingLineEdge =

-someRacingLineRadius - someOffsetBeyondRacingLine;

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eLeftEdge, leftRacingLineEdge);

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eCenter, -someOffsetBeyondRacingLine);

float rightRacingLineEdge =

someRacingLineRadius - someOffsetBeyondRacingLine;

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eRightEdge, rightRacingLineEdge);

! OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

! float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(idealRacingLineOffset, someRacingLineRadius);

}

void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = new RacingLineOffsets(); float leftRacingLineEdge = -someRacingLineRadius - someOffsetWithinRadius; withinOfffsets.setSignedDistanceToRacingLine( RacingLine.eLeftEdge, leftRacingLineEdge); withinOffsets.setSignedDistanceToRacingLine( RacingLine.eCenter, -someOffsetWithinRadius); float rightRacingLineEdge = someRacingLineRadius - someOffsetWithinRadius; withinOffsets.setSignedDistanceToRacingLine( RacingLine.eRightEdge, rightRacingLineEdge); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL(idealRacingLineOffset, someOffsetWithinRadius); }

#pracunittests

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f;

! RacingLineOffsets beyondOffsets = new RacingLineOffsets();

float leftRacingLineEdge =

-someRacingLineRadius - someOffsetBeyondRacingLine;

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eLeftEdge, leftRacingLineEdge);

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eCenter, -someOffsetBeyondRacingLine);

float rightRacingLineEdge =

someRacingLineRadius - someOffsetBeyondRacingLine;

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eRightEdge, rightRacingLineEdge);

! OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

! float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(idealRacingLineOffset, someRacingLineRadius);

}

void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() { float someRacingLineRadius = 10.0f; float someOffsetWithinRadius = someRacingLineRadius * 0.8f; ! RacingLineOffsets withinOffsets = new RacingLineOffsets(); float leftRacingLineEdge = -someRacingLineRadius - someOffsetWithinRadius; withinOfffsets.setSignedDistanceToRacingLine( RacingLine.eLeftEdge, leftRacingLineEdge); withinOffsets.setSignedDistanceToRacingLine( RacingLine.eCenter, -someOffsetWithinRadius); float rightRacingLineEdge = someRacingLineRadius - someOffsetWithinRadius; withinOffsets.setSignedDistanceToRacingLine( RacingLine.eRightEdge, rightRacingLineEdge); ! OffsetRequest idealRequest = m_raceBehaviour.getIdealOffset(withinOffsets); ! float idealRacingLineOffset = idealRequest.GetOffset(); TEST_EQUAL(idealRacingLineOffset, someOffsetWithinRadius); }

Not DRY

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, withinOffsets.RightEdge);

}

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, withinOffsets.RightEdge);

}

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, withinOffsets.RightEdge);

}

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

test_tauOffsetEqual(idealRequest, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

test_racingLineOffsetEqual(

idealRequest, withinOffsets.RightEdge);

}

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

test_tauOffsetEqual(idealRequest, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

test_racingLineOffsetEqual(

idealRequest, withinOffsets.RightEdge);

}

#pracunittests

The Wet Anti-Pattern

#pracunittests

The Wet Anti-Pattern• Hard-to-maintain hacky tests?

#pracunittests

The Wet Anti-Pattern• Hard-to-maintain hacky tests?

• Keep production sensibilities in unit test code

#pracunittests

The Wet Anti-Pattern• Hard-to-maintain hacky tests?

• Keep production sensibilities in unit test code

• Stay DRY with helper functions and custom asserts

#pracunittests

The Wet Anti-Pattern• Hard-to-maintain hacky tests?

• Keep production sensibilities in unit test code

• Stay DRY with helper functions and custom asserts

• Do not hide the call to the function under test

#pracunittests

3/4: The Deep Anti-Pattern

> Test failed: > getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets > With Assert: VehicleID 0 != 1

#pracunittests

void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleID someOwnerID = (VehicleID)1; LinearDescription ownedDescription = DefaultFlatLinearDescription().withOwner(someOwnerID); !

float someNegativeOffset = -1.0f; float ownerAtNegativeOffset = ownedDescription.getOwnerAtOffset(someNegativeOffset); float somePositiveOffset = 1.0f; float ownerAtPositiveOffset = ownedDescription.getOwnerAtOffset(somePositiveOffset); !

test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID); test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID); }

#pracunittests

void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleID someOwnerID = (VehicleID)1; LinearDescription ownedDescription = DefaultFlatLinearDescription().withOwner(someOwnerID); !

float someNegativeOffset = -1.0f; float ownerAtNegativeOffset = ownedDescription.getOwnerAtOffset(someNegativeOffset); float somePositiveOffset = 1.0f; float ownerAtPositiveOffset = ownedDescription.getOwnerAtOffset(somePositiveOffset); !

test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID); test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID); }

#pracunittests

void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleID someOwnerID = (VehicleID)1; LinearDescription ownedDescription = DefaultFlatLinearDescription().withOwner(someOwnerID); !

float someNegativeOffset = -1.0f; float ownerAtNegativeOffset = ownedDescription.getOwnerAtOffset(someNegativeOffset); float somePositiveOffset = 1.0f; float ownerAtPositiveOffset = ownedDescription.getOwnerAtOffset(somePositiveOffset); !

test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID); test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID); }

#pracunittests

void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() { VehicleID someOwnerID = (VehicleID)1; LinearDescription ownedDescription = DefaultFlatLinearDescription().withOwner(someOwnerID); !

float someNegativeOffset = -1.0f; float ownerAtNegativeOffset = ownedDescription.getOwnerAtOffset(someNegativeOffset); float somePositiveOffset = 1.0f; float ownerAtPositiveOffset = ownedDescription.getOwnerAtOffset(somePositiveOffset); !

test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, someOwnerID); test_OwningVehicleIsEqualTo(ownerAtPositiveOffset, someOwnerID); }

>1 explicit

assumption

#pracunittests

Deep: One Assert Per Testvoid getOwnerAtOffset_WithOwnerAndNegativeOffset_ReturnsOwner() { float someNegativeOffset = -1.0f; !

float ownerAtNegativeOffset = m_ownedDescription.getOwnerAtOffset(someNegativeOffset); !

test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, m_someOwnerID); } !

void getOwnerAtOffset_WithOwnerAndPositiveOffset_ReturnsOwner() { // *snip* }

#pracunittests

> Test failed: > getOwnerAtOffset_WithOwnerAndNegativeOffset_ReturnsOwner > With Assert: VehicleID 0 != 1 > Test failed: > getOwnerAtOffset_WithOwnerAndPositiveOffset_ReturnsOwner > With Assert: VehicleID 0 != 1

#pracunittests

The Deep Anti-Pattern

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

• Too many explicit assumptions per test

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

• Too many explicit assumptions per test

• Minimise assumptions per test

#pracunittests

4/4: The Wide Anti-Pattern

#pracunittests

4/4: The Wide Anti-Pattern

> Executed 613 test(s), 599 test(s) passed, 14 test(s) failed.

#pracunittests

// in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); !

BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); !

behaviourSystem.updateWithBlackboard(worldInfo); !

MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }

#pracunittests

// in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); !

BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); !

behaviourSystem.updateWithBlackboard(worldInfo); !

MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }

#pracunittests

// in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); !

BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); !

behaviourSystem.updateWithBlackboard(worldInfo); !

MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }

#pracunittests

// in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); !

BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); !

behaviourSystem.updateWithBlackboard(worldInfo); !

MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }

>0 implicit

assumptions

#pracunittests

// in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); !

BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); !

behaviourSystem.updateWithBlackboard(worldInfo); !

MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }

#pracunittests

// in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); !

BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); !

behaviourSystem.updateWithBlackboard(worldInfo); !

MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }

#pracunittests

// in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); !

BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); !

behaviourSystem.updateWithBlackboard(worldInfo); !

MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }

#pracunittests

// in DraftBehaviourFixture… void updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); !

BehaviourSystem behaviourSystem = new BehaviourSystem(); behaviourSystem.addBehaviour(draft); !

behaviourSystem.updateWithBlackboard(worldInfo); !

MovementRequest draftMovement = behaviourSystem.getMovementRequest(); TEST_EQUAL(draftMovement.desiredRacingLineOffset, someDraftTargetVehicleOffset); }

#pracunittests

Wide: Seamsvoid DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);

Game Code

#pracunittests

Wide: Seamsvoid DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);

class HeatMap { virtual void WriteHeat(float offset, float value) { … } }

Game Code

#pracunittests

Wide: Seamsvoid DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);

class HeatMap { virtual void WriteHeat(float offset, float value) { … } }

class MockHeatMap : HeatMap { override void WriteHeat(float offset, float value) { … } }

Game Code

Test Library

#pracunittests

Wide: Seamsvoid DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);

class HeatMap { virtual void WriteHeat(float offset, float value) { … } }

class MockHeatMap : HeatMap { override void WriteHeat(float offset, float value) { … } }

Game Code

Test Library

#pracunittests

Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); MockHeatMap mockMap = new MockHeatMap(); !

draft.updateImpl(worldInfo, mockMap); !

float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset(); TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset); }

#pracunittests

Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); MockHeatMap mockMap = new MockHeatMap(); !

draft.updateImpl(worldInfo, mockMap); !

float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset(); TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset); }

#pracunittests

Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() { WorldInfo worldInfo = new WorldInfo(); float someDraftTargetVehicleOffset = 2.0f; // *snip* blackboard setup !

DraftBehaviour draft = new DraftBehaviour(); MockHeatMap mockMap = new MockHeatMap(); !

draft.updateImpl(worldInfo, mockMap); !

float draftOffsetWithHighestHeat = mockMap.getHighestHeatOffset(); TEST_EQUAL(draftOffsetWithHighestHeat, someDraftTargetVehicleOffset); }

#pracunittests

The Wide Anti-Pattern

#pracunittests

The Wide Anti-Pattern• False-negative test failures?

#pracunittests

The Wide Anti-Pattern• False-negative test failures?

• Many implicit assumptions

#pracunittests

The Wide Anti-Pattern• False-negative test failures?

• Many implicit assumptions

• Isolate code with seams, to enable simple fake impostors

#pracunittests

Recap

#pracunittests

Recap• Respect unit test source code as much as

production source code

#pracunittests

Recap• Respect unit test source code as much as

production source code

• Write once, read many

#pracunittests

Recap• Respect unit test source code as much as

production source code

• Write once, read many

• Only 1 explicit assumption

#pracunittests

Recap• Respect unit test source code as much as

production source code

• Write once, read many

• Only 1 explicit assumption

• Minimise implicit assumptions

#pracunittests

• 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

Recommended