Top Banner
http://tinyurl.com/sf-rnt http://spryfox.com/our-games/road-not-taken/ shut down wifi, skype. RECORD!
107
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Practical unit testing 2014

http://tinyurl.com/sf-rnt

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

Page 2: Practical unit testing 2014

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

Page 3: Practical unit testing 2014

#pracunittests

Test Driven Development

move on!

Page 4: Practical unit testing 2014

#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

Page 5: Practical unit testing 2014

#pracunittests

2004

@tenpn

Page 6: Practical unit testing 2014

#pracunittests

2004

@tenpn

Page 7: Practical unit testing 2014

Definitions

Page 8: Practical unit testing 2014

#pracunittests

Unit TestSingle explicit assumption

IMPORTANT functional/integration interchangable

Page 9: Practical unit testing 2014

#pracunittests

Unit TestSingle explicit assumption

Integration TestMany implicit assumptions

IMPORTANT functional/integration interchangable

Page 10: Practical unit testing 2014

#pracunittests

Qualities of Good Unit Tests

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

Page 11: Practical unit testing 2014

#pracunittests

Qualities of Good Unit Tests

Readable

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

Page 12: Practical unit testing 2014

#pracunittests

Qualities of Good Unit Tests

Readable

Maintainable

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

Page 13: Practical unit testing 2014

#pracunittests

Qualities of Good Unit Tests

Readable

MaintainableTrustworthy

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

Page 14: Practical unit testing 2014

PostMortem

Page 15: Practical unit testing 2014

#pracunittests

F1 2011 X360/PS3/PC

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

Page 16: Practical unit testing 2014

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

Page 17: Practical unit testing 2014

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

• 502 tests, 6700 lines of test code

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

Page 18: Practical unit testing 2014

#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

Page 19: Practical unit testing 2014

#pracunittests

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

Page 20: Practical unit testing 2014

#pracunittests

• Clean, re-usable code

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

Page 21: Practical unit testing 2014

#pracunittests

• Clean, re-usable code

• Fewer bugs

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

Page 22: Practical unit testing 2014

#pracunittests

• Clean, re-usable code

• Fewer bugs

• Easy to optimise

A Partial Succes

PRODUCTION CODE => SUBSYSTEM CODEHIGHLIGHT loc shipped 4+ previously

Page 23: Practical unit testing 2014

#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

Page 24: Practical unit testing 2014

Unit TestAnti-Patterns

Page 25: Practical unit testing 2014

#pracunittests

1/4: The Opaque Anti-Pattern

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

Page 26: Practical unit testing 2014

#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

Page 27: Practical unit testing 2014

#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

Page 28: Practical unit testing 2014

#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

Page 29: Practical unit testing 2014

#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

Page 30: Practical unit testing 2014

#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

Page 31: Practical unit testing 2014

#pracunittests

Opaque: No Magic Literals

break the rules when you need to!

Page 32: Practical unit testing 2014

#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!

Page 33: Practical unit testing 2014

#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!

Page 34: Practical unit testing 2014

#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!

Page 35: Practical unit testing 2014

#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!

Page 36: Practical unit testing 2014

#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); }

Page 37: Practical unit testing 2014

#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); }

Page 38: Practical unit testing 2014

#pracunittests

Opaque: Informative, Consistent Test Name

Page 39: Practical unit testing 2014

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

Page 40: Practical unit testing 2014

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

void testBackwardsToNormalLeftwardsGradient() {

Page 41: Practical unit testing 2014

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

void

void withDirection_Left_InvertsGradient() {

Page 42: Practical unit testing 2014

#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); }

Page 43: Practical unit testing 2014

#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); }

Page 44: Practical unit testing 2014

#pracunittests

Opaque: Arrange-Act-Assert

Page 45: Practical unit testing 2014

#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); }

Page 46: Practical unit testing 2014

#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); }

Page 47: Practical unit testing 2014

#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); }

Page 48: Practical unit testing 2014

#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); }

Page 49: Practical unit testing 2014

#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); }

Page 50: Practical unit testing 2014

#pracunittests

The Opaque Anti-Pattern

breathe at end!

Page 51: Practical unit testing 2014

#pracunittests

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

breathe at end!

Page 52: Practical unit testing 2014

#pracunittests

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

• Demystify magic literals

breathe at end!

Page 53: Practical unit testing 2014

#pracunittests

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

• Demystify magic literals

• Consistent informative test name

breathe at end!

Page 54: Practical unit testing 2014

#pracunittests

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

• Demystify magic literals

• Consistent informative test name

• Arrange-Act-Assert

breathe at end!

Page 55: Practical unit testing 2014

#pracunittests

2/4: The Wet Anti-Pattern

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)

On refactoring

Page 56: Practical unit testing 2014

#pracunittests

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)

2/4: The Wet Anti-Pattern

RacingLineOffsets

On refactoring

Page 57: Practical unit testing 2014

#pracunittests

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)

2/4: The Wet Anti-Pattern

> Test library build failed with 235 error(s)

RacingLineOffsets

On refactoring

Page 58: Practical unit testing 2014

#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); }

Page 59: Practical unit testing 2014

#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

Page 60: Practical unit testing 2014

#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);

}

Page 61: Practical unit testing 2014

#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);

}

Page 62: Practical unit testing 2014

#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);

}

Page 63: Practical unit testing 2014

#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);

}

Page 64: Practical unit testing 2014

#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);

}

Page 65: Practical unit testing 2014

#pracunittests

The Wet Anti-Pattern

breathe at end!

Page 66: Practical unit testing 2014

#pracunittests

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

breathe at end!

Page 67: Practical unit testing 2014

#pracunittests

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

• Keep production sensibilities in unit test code

breathe at end!

Page 68: Practical unit testing 2014

#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!

Page 69: Practical unit testing 2014

#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!

Page 70: Practical unit testing 2014

#pracunittests

3/4: The Deep Anti-Pattern

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

on introducing a bug chi == “kai”!

Page 71: Practical unit testing 2014

#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

Page 72: Practical unit testing 2014

#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

Page 73: Practical unit testing 2014

#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

Page 74: Practical unit testing 2014

#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

Page 75: Practical unit testing 2014

#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* }

Page 76: Practical unit testing 2014

#pracunittests

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

Page 77: Practical unit testing 2014

#pracunittests

The Deep Anti-Pattern

breathe at end!

Page 78: Practical unit testing 2014

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

breathe at end!

Page 79: Practical unit testing 2014

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

• Too many explicit assumptions per test

breathe at end!

Page 80: Practical unit testing 2014

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

• Too many explicit assumptions per test

• Minimise assumptions per test

breathe at end!

Page 81: Practical unit testing 2014

#pracunittests

4/4: The Wide Anti-Pattern

On introducing a bug in behaviour system

Page 82: Practical unit testing 2014

#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

Page 83: Practical unit testing 2014

#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); }

Page 84: Practical unit testing 2014

#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); }

Page 85: Practical unit testing 2014

#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); }

Page 86: Practical unit testing 2014

#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

Page 87: Practical unit testing 2014

#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); }

Page 88: Practical unit testing 2014

#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); }

Page 89: Practical unit testing 2014

#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); }

Page 90: Practical unit testing 2014

#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); }

Page 91: Practical unit testing 2014

#pracunittests

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

Game Code

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

Page 92: Practical unit testing 2014

#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

Page 93: Practical unit testing 2014

#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

Page 94: Practical unit testing 2014

#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

Page 95: Practical unit testing 2014

#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); }

Page 96: Practical unit testing 2014

#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); }

Page 97: Practical unit testing 2014

#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); }

Page 98: Practical unit testing 2014

#pracunittests

The Wide Anti-Pattern

breathe at end!

Page 99: Practical unit testing 2014

#pracunittests

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

breathe at end!

Page 100: Practical unit testing 2014

#pracunittests

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

• Many implicit assumptions

breathe at end!

Page 101: Practical unit testing 2014

#pracunittests

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

• Many implicit assumptions

• Isolate code with seams, to enable simple fake impostors

breathe at end!

Page 102: Practical unit testing 2014

#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

Page 103: Practical unit testing 2014

#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

Page 104: Practical unit testing 2014

#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

Page 105: Practical unit testing 2014

#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

Page 106: Practical unit testing 2014

#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

Page 107: Practical unit testing 2014

#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!