Top Banner
http://tinyurl.com/sf-rnt
107

Practical unit testing GDC 2014

Jan 14, 2015

Download

Technology

Andrew Fray

Slides from my GDC 2014 talk on practical unit testing. How can bad unit tests slow iteration? And how can you fix them?
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 GDC 2014

http://tinyurl.com/sf-rnt

Page 2: Practical unit testing GDC 2014

UnitTests

Practical

Andrew Fray, Spry Fox

Page 3: Practical unit testing GDC 2014

#pracunittests

Test Driven Development

Page 4: Practical unit testing GDC 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 GDC 2014

#pracunittests

2004

@tenpn

Page 6: Practical unit testing GDC 2014

#pracunittests

2004

@tenpn

Page 7: Practical unit testing GDC 2014

Definitions

Page 8: Practical unit testing GDC 2014

#pracunittests

Unit TestSingle explicit assumption

Page 9: Practical unit testing GDC 2014

#pracunittests

Unit TestSingle explicit assumption

Integration TestMany implicit assumptions

Page 10: Practical unit testing GDC 2014

#pracunittests

Qualities of Good Unit Tests

Page 11: Practical unit testing GDC 2014

#pracunittests

Qualities of Good Unit Tests

Readable

Page 12: Practical unit testing GDC 2014

#pracunittests

Qualities of Good Unit Tests

Readable

Maintainable

Page 13: Practical unit testing GDC 2014

#pracunittests

Qualities of Good Unit Tests

Readable

MaintainableTrustworthy

Page 14: Practical unit testing GDC 2014

PostMortem

Page 15: Practical unit testing GDC 2014

#pracunittests

F1 2011 X360/PS3/PC

Page 16: Practical unit testing GDC 2014

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

Page 17: Practical unit testing GDC 2014

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

• 502 tests, 6700 lines of test code

Page 18: Practical unit testing GDC 2014

#pracunittests

F1 2011 X360/PS3/PC

• Isolated new subsystem

• 502 tests, 6700 lines of test code

• 6200 lines of production code

Page 19: Practical unit testing GDC 2014

#pracunittests

A Partial Succes

Page 20: Practical unit testing GDC 2014

#pracunittests

• Clean, re-usable code

A Partial Succes

Page 21: Practical unit testing GDC 2014

#pracunittests

• Clean, re-usable code

• Fewer bugs

A Partial Succes

Page 22: Practical unit testing GDC 2014

#pracunittests

• Clean, re-usable code

• Fewer bugs

• Easy to optimise

A Partial Succes

Page 23: Practical unit testing GDC 2014

#pracunittests

• Clean, re-usable code

• Fewer bugs

• Easy to optimise

• At end, treacle-like progress

A Partial Succes

Page 24: Practical unit testing GDC 2014

Unit TestAnti-Patterns

Page 25: Practical unit testing GDC 2014

#pracunittests

1/4: The Opaque Anti-Pattern

Page 26: Practical unit testing GDC 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

Page 27: Practical unit testing GDC 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

Page 28: Practical unit testing GDC 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); }

Page 29: Practical unit testing GDC 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); }

Page 30: Practical unit testing GDC 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); }

Page 31: Practical unit testing GDC 2014

#pracunittests

Opaque: No Magic Literals

Page 32: Practical unit testing GDC 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); }

Page 33: Practical unit testing GDC 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); }

Page 34: Practical unit testing GDC 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); }

Page 35: Practical unit testing GDC 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); }

Page 36: Practical unit testing GDC 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 GDC 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 GDC 2014

#pracunittests

Opaque: Informative, Consistent Test Name

Page 39: Practical unit testing GDC 2014

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

Page 40: Practical unit testing GDC 2014

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

void testBackwardsToNormalLeftwardsGradient() {

Page 41: Practical unit testing GDC 2014

#pracunittests

Opaque: Informative, Consistent Test Name

void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() {

void

void withDirection_Left_InvertsGradient() {

Page 42: Practical unit testing GDC 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 GDC 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 GDC 2014

#pracunittests

Opaque: Arrange-Act-Assert

Page 45: Practical unit testing GDC 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 GDC 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 GDC 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 GDC 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 GDC 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 GDC 2014

#pracunittests

The Opaque Anti-Pattern

Page 51: Practical unit testing GDC 2014

#pracunittests

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

Page 52: Practical unit testing GDC 2014

#pracunittests

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

• Demystify magic literals

Page 53: Practical unit testing GDC 2014

#pracunittests

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

• Demystify magic literals

• Consistent informative test name

Page 54: Practical unit testing GDC 2014

#pracunittests

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

• Demystify magic literals

• Consistent informative test name

• Arrange-Act-Assert

Page 55: Practical unit testing GDC 2014

#pracunittests

2/4: The Wet Anti-Pattern

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)

Page 56: Practical unit testing GDC 2014

#pracunittests

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)

2/4: The Wet Anti-Pattern

RacingLineOffsets

Page 57: Practical unit testing GDC 2014

#pracunittests

RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)

2/4: The Wet Anti-Pattern

> Test library build failed with 235 error(s)

RacingLineOffsets

Page 58: Practical unit testing GDC 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 GDC 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 GDC 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 GDC 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 GDC 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 GDC 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 GDC 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 GDC 2014

#pracunittests

The Wet Anti-Pattern

Page 66: Practical unit testing GDC 2014

#pracunittests

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

Page 67: Practical unit testing GDC 2014

#pracunittests

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

• Keep production sensibilities in unit test code

Page 68: Practical unit testing GDC 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

Page 69: Practical unit testing GDC 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

Page 70: Practical unit testing GDC 2014

#pracunittests

3/4: The Deep Anti-Pattern

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

Page 71: Practical unit testing GDC 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); }

Page 72: Practical unit testing GDC 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); }

Page 73: Practical unit testing GDC 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); }

Page 74: Practical unit testing GDC 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

Page 75: Practical unit testing GDC 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 GDC 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 GDC 2014

#pracunittests

The Deep Anti-Pattern

Page 78: Practical unit testing GDC 2014

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

Page 79: Practical unit testing GDC 2014

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

• Too many explicit assumptions per test

Page 80: Practical unit testing GDC 2014

#pracunittests

The Deep Anti-Pattern

• Test failures not fully informative?

• Too many explicit assumptions per test

• Minimise assumptions per test

Page 81: Practical unit testing GDC 2014

#pracunittests

4/4: The Wide Anti-Pattern

Page 82: Practical unit testing GDC 2014

#pracunittests

4/4: The Wide Anti-Pattern

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

Page 83: Practical unit testing GDC 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 GDC 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 GDC 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 GDC 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 GDC 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 GDC 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 GDC 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 GDC 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 GDC 2014

#pracunittests

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

Game Code

Page 92: Practical unit testing GDC 2014

#pracunittests

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

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

Game Code

Page 93: Practical unit testing GDC 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

Page 94: Practical unit testing GDC 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

Page 95: Practical unit testing GDC 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 GDC 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 GDC 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 GDC 2014

#pracunittests

The Wide Anti-Pattern

Page 99: Practical unit testing GDC 2014

#pracunittests

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

Page 100: Practical unit testing GDC 2014

#pracunittests

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

• Many implicit assumptions

Page 101: Practical unit testing GDC 2014

#pracunittests

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

• Many implicit assumptions

• Isolate code with seams, to enable simple fake impostors

Page 102: Practical unit testing GDC 2014

#pracunittests

Recap

Page 103: Practical unit testing GDC 2014

#pracunittests

Recap• Respect unit test source code as much as

production source code

Page 104: Practical unit testing GDC 2014

#pracunittests

Recap• Respect unit test source code as much as

production source code

• Write once, read many

Page 105: Practical unit testing GDC 2014

#pracunittests

Recap• Respect unit test source code as much as

production source code

• Write once, read many

• Only 1 explicit assumption

Page 106: Practical unit testing GDC 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

Page 107: Practical unit testing GDC 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