107
http://tinyurl.com/sf-rnt http://spryfox.com/our-games/road-not-taken/ shut down wifi, skype. RECORD!

Practical unit testing 2014

Embed Size (px)

DESCRIPTION

 

Citation preview

http://tinyurl.com/sf-rnt

http://spryfox.com/our-games/road-not-taken/ shut down wifi, skype. RECORD!

UnitTests

Practical

Andrew Fray, Spry Fox

Thank you for coming. This is Practical Unit Tests, and I’m Andrew Fray. My talk today is about how to structure your unit tests to aid iteration. We’re going to be looking in detail at my unit tests from a shipped AAA console project, the mistakes I made in writing them, and the concrete costs they caused. Then we’ll look at simple ways to avoid those costs in your own unit tests.!definitions Post mortem Anti-patterns

#pracunittests

Test Driven Development

move on!

#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

IMPORTANT functional/integration interchangable

#pracunittests

Unit TestSingle explicit assumption

Integration TestMany implicit assumptions

IMPORTANT functional/integration interchangable

#pracunittests

Qualities of Good Unit Tests

Readable - why. up to speed. Maintainable - how. modify for iterating. Trustworthy - quick, deterministic. no external resources.

#pracunittests

Qualities of Good Unit Tests

Readable

Readable - why. up to speed. Maintainable - how. modify for iterating. Trustworthy - quick, deterministic. no external resources.

#pracunittests

Qualities of Good Unit Tests

Readable

Maintainable

Readable - why. up to speed. Maintainable - how. modify for iterating. Trustworthy - quick, deterministic. no external resources.

#pracunittests

Qualities of Good Unit Tests

Readable

MaintainableTrustworthy

Readable - why. up to speed. Maintainable - how. modify for iterating. Trustworthy - quick, deterministic. no external resources.

PostMortem

#pracunittests

F1 2011 X360/PS3/PC

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

• 502 tests, 6700 lines of test code

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

• 502 tests, 6700 lines of test code

• 6200 lines of production code

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

#pracunittests

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

#pracunittests

• Clean, re-usable code

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

#pracunittests

• Clean, re-usable code

• Fewer bugs

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

#pracunittests

• Clean, re-usable code

• Fewer bugs

• Easy to optimise

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

#pracunittests

• Clean, re-usable code

• Fewer bugs

• Easy to optimise

• At end, treacle-like progress

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

Unit TestAnti-Patterns

#pracunittests

1/4: The Opaque Anti-Pattern

types: light grey names: bold dark grey literals: red else: dark grey

#pracunittests

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

1/4: The Opaque Anti-Pattern

types: light grey names: bold dark grey literals: red else: dark grey

#pracunittests

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

1/4: The Opaque Anti-Pattern

types: light grey names: bold dark grey literals: red else: dark grey

#pracunittests

Opaque: Hard to see HOW

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

types: light grey names: bold dark grey literals: red else: dark grey

#pracunittests

Opaque: Hard to see HOW

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

types: light grey names: bold dark grey literals: red else: dark grey

#pracunittests

Opaque: Hard to see HOW

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

types: light grey names: bold dark grey literals: red else: dark grey

#pracunittests

Opaque: No Magic Literals

break the rules when you need to!

#pracunittests

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

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

break the rules when you need to!

#pracunittests

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

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

break the rules when you need to!

#pracunittests

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

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

break the rules when you need to!

#pracunittests

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

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

break the rules when you need to!

#pracunittests

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

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

#pracunittests

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

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

#pracunittests

Opaque: Informative, Consistent Test Name

#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

breathe at end!

#pracunittests

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

breathe at end!

#pracunittests

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

• Demystify magic literals

breathe at end!

#pracunittests

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

• Demystify magic literals

• Consistent informative test name

breathe at end!

#pracunittests

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

• Demystify magic literals

• Consistent informative test name

• Arrange-Act-Assert

breathe at end!

#pracunittests

2/4: The Wet Anti-Pattern

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)

On refactoring

#pracunittests

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)

2/4: The Wet Anti-Pattern

RacingLineOffsets

On refactoring

#pracunittests

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)

2/4: The Wet Anti-Pattern

> Test library build failed with 235 error(s)

RacingLineOffsets

On refactoring

#pracunittests

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f;

! RacingLineOffsets beyondOffsets = new RacingLineOffsets();

float leftRacingLineEdge =

-someRacingLineRadius - someOffsetBeyondRacingLine;

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eLeftEdge, leftRacingLineEdge);

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eCenter, -someOffsetBeyondRacingLine);

float rightRacingLineEdge =

someRacingLineRadius - someOffsetBeyondRacingLine;

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eRightEdge, rightRacingLineEdge);

! OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

! float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(idealRacingLineOffset, someRacingLineRadius);

}

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

#pracunittests

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine = someRacingLineRadius + 1.0f;

! RacingLineOffsets beyondOffsets = new RacingLineOffsets();

float leftRacingLineEdge =

-someRacingLineRadius - someOffsetBeyondRacingLine;

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eLeftEdge, leftRacingLineEdge);

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eCenter, -someOffsetBeyondRacingLine);

float rightRacingLineEdge =

someRacingLineRadius - someOffsetBeyondRacingLine;

beyondOffsets.setSignedDistanceToRacingLine(

RacingLine.eRightEdge, rightRacingLineEdge);

! OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

! float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(idealRacingLineOffset, someRacingLineRadius);

}

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

Not DRY

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, withinOffsets.RightEdge);

}

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, withinOffsets.RightEdge);

}

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

float idealRacingLineOffset = idealRequest.GetOffset();

TEST_EQUAL(

idealRacingLineOffset, withinOffsets.RightEdge);

}

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

test_tauOffsetEqual(idealRequest, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

test_racingLineOffsetEqual(

idealRequest, withinOffsets.RightEdge);

}

#pracunittests

Wet: Helper Functionsvoid getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {

float someRacingLineRadius = 10.0f;

float someOffsetWithinRadius =

someRacingLineRadius * 0.8f;

!

RacingLineOffsets withinOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetWithinRadius);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(withinOffsets);

!

test_tauOffsetEqual(idealRequest, someOffsetWithinRadius);

}

void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {

float someRacingLineRadius = 10.0f;

float someOffsetBeyondRacingLine =

someRacingLineRadius + 1.0f;

!

RacingLineOffsets beyondOffsets = CreateRacingLineOffsets(

someRacingLineRadius, someOffsetBeyondRacingLine);

!

OffsetRequest idealRequest =

m_raceBehaviour.getIdealOffset(beyondOffsets);

!

test_racingLineOffsetEqual(

idealRequest, withinOffsets.RightEdge);

}

#pracunittests

The Wet Anti-Pattern

breathe at end!

#pracunittests

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

breathe at end!

#pracunittests

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

• Keep production sensibilities in unit test code

breathe at end!

#pracunittests

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

• Keep production sensibilities in unit test code

• Stay DRY with helper functions and custom asserts

breathe at end!

#pracunittests

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

• Keep production sensibilities in unit test code

• Stay DRY with helper functions and custom asserts

• Do not hide the call to the function under test

breathe at end!

#pracunittests

3/4: The Deep Anti-Pattern

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

on introducing a bug chi == “kai”!

#pracunittests

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

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

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

make it clear

#pracunittests

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

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

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

make it clear

#pracunittests

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

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

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

make it clear

#pracunittests

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

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

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

>1 explicit

assumption

make it clear

#pracunittests

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

float ownerAtNegativeOffset = m_ownedDescription.getOwnerAtOffset(someNegativeOffset); !

test_OwningVehicleIsEqualTo(ownerAtNegativeOffset, m_someOwnerID); } !

void getOwnerAtOffset_WithOwnerAndPositiveOffset_ReturnsOwner() { // *snip* }

#pracunittests

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

#pracunittests

The Deep Anti-Pattern

breathe at end!

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

breathe at end!

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

• Too many explicit assumptions per test

breathe at end!

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

• Too many explicit assumptions per test

• Minimise assumptions per test

breathe at end!

#pracunittests

4/4: The Wide Anti-Pattern

On introducing a bug in behaviour system

#pracunittests

4/4: The Wide Anti-Pattern

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

On introducing a bug in behaviour system

#pracunittests

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

DraftBehaviour draft = new DraftBehaviour(); !

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

behaviourSystem.updateWithBlackboard(worldInfo); !

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

#pracunittests

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

DraftBehaviour draft = new DraftBehaviour(); !

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

behaviourSystem.updateWithBlackboard(worldInfo); !

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

#pracunittests

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

DraftBehaviour draft = new DraftBehaviour(); !

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

behaviourSystem.updateWithBlackboard(worldInfo); !

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

#pracunittests

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

DraftBehaviour draft = new DraftBehaviour(); !

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

behaviourSystem.updateWithBlackboard(worldInfo); !

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

>0 implicit

assumptions

#pracunittests

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

DraftBehaviour draft = new DraftBehaviour(); !

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

behaviourSystem.updateWithBlackboard(worldInfo); !

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

#pracunittests

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

DraftBehaviour draft = new DraftBehaviour(); !

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

behaviourSystem.updateWithBlackboard(worldInfo); !

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

#pracunittests

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

DraftBehaviour draft = new DraftBehaviour(); !

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

behaviourSystem.updateWithBlackboard(worldInfo); !

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

#pracunittests

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

DraftBehaviour draft = new DraftBehaviour(); !

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

behaviourSystem.updateWithBlackboard(worldInfo); !

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

#pracunittests

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

Game Code

Inversion of Control Interfaces/virtual functions Mocking frameworks/helper classes

#pracunittests

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

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

Game Code

Inversion of Control Interfaces/virtual functions Mocking frameworks/helper classes

#pracunittests

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

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

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

Game Code

Test Library

Inversion of Control Interfaces/virtual functions Mocking frameworks/helper classes

#pracunittests

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

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

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

Game Code

Test Library

Inversion of Control Interfaces/virtual functions Mocking frameworks/helper classes

#pracunittests

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

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

draft.updateImpl(worldInfo, mockMap); !

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

#pracunittests

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

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

draft.updateImpl(worldInfo, mockMap); !

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

#pracunittests

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

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

draft.updateImpl(worldInfo, mockMap); !

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

#pracunittests

The Wide Anti-Pattern

breathe at end!

#pracunittests

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

breathe at end!

#pracunittests

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

• Many implicit assumptions

breathe at end!

#pracunittests

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

• Many implicit assumptions

• Isolate code with seams, to enable simple fake impostors

breathe at end!

#pracunittests

Recap

Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions

#pracunittests

Recap• Respect unit test source code as much as

production source code

Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions

#pracunittests

Recap• Respect unit test source code as much as

production source code

• Write once, read many

Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions

#pracunittests

Recap• Respect unit test source code as much as

production source code

• Write once, read many

• Only 1 explicit assumption

Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions

#pracunittests

Recap• Respect unit test source code as much as

production source code

• Write once, read many

• Only 1 explicit assumption

• Minimise implicit assumptions

Respect unit test quality as much as production code quality Write once, read many Only 1 explicit assumption As few as possible implicit assumptions

#pracunittests

[email protected]

• @tenpn

• andrewfray.wordpress.com

• Roy Osherove: Art of Unit Testing www.artofunittesting.com

• Michael Feathers: Working Effectively with Legacy Code

• Steve Freeman & Nat Pryce: Growing Object-Orientated Software, Guided By Tests

Colour scheme by Miaka www.colourlovers.com/palette/444487/Curiosity_Killed

Feedback! Thank the CAs!