Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
http://tinyurl.com/sf-rnt
Unit
Tests
Practical
Andrew Fray, Spry Fox
#pracunittests
Test Driven
Development
#pracunittests
Backwards Is Forward: Making Better
Games with Test-Driven Development
http://gdcvault.com/play/1013416/Bac...
#pracunittests
2004
@tenpn
#pracunittests
2004
@tenpn
Definitions
#pracunittests
Unit TestSingle explicit assumption
#pracunittests
Unit TestSingle explicit assumption
Integration Test
Many implicit assumptions
#pracunittests
Qualities of Good Unit Tests
#pracunittests
Qualities of Good Unit Tests
Readable
#pracunittests
Qualities of Good Unit Tests
Readable
Maintainable
#pracunittests
Qualities of Good Unit Tests
Readable
Maintainable
Trustworthy
Post
Mortem
#pracunittests
F1 2011 X360/PS3/PC
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
• 502 tests, 6700 lines of test code
#pracunittests
F1 2011 X360/PS3/PC
• Isolated new subsystem
• 502 tests, 6700 lines of test code
• 6200 lines of productio...
#pracunittests
A Partial Succes
#pracunittests
• Clean, re-usable
code
A Partial Succes
#pracunittests
• Clean, re-usable
code
• Fewer bugs
A Partial Succes
#pracunittests
• Clean, re-usable
code
• Fewer bugs
• Easy to optimise
A Partial Succes
#pracunittests
• Clean, re-usable
code
• Fewer bugs
• Easy to optimise
• At end, treacle-like
progress
A Partial Succes
Unit Test
Anti-Patterns
#pracunittests
1/4: The Opaque Anti-Pattern
#pracunittests
// in LinearDescriptionFixture…
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc ...
#pracunittests
// in LinearDescriptionFixture…
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc ...
#pracunittests
Opaque: Hard to see HOW
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc = Defaul...
#pracunittests
Opaque: Hard to see HOW
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc = Defaul...
#pracunittests
Opaque: Hard to see HOW
void testBackwardsToNormalLeftwardsGradient() {
LinearDescription leftDesc = Defaul...
#pracunittests
Opaque: No Magic Literals
#pracunittests
Opaque: No Magic Literals
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
...
#pracunittests
Opaque: No Magic Literals
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
...
#pracunittests
Opaque: No Magic Literals
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
...
#pracunittests
Opaque: No Magic Literals
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
...
#pracunittests
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
f...
#pracunittests
void testBackwardsToNormalLeftwardsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
f...
#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() ...
#pracunittests
Opaque: Informative,
Consistent Test Name
void nameOfFunctionUnderTest_ContextOfTest_DesiredResultOfTest() ...
#pracunittests
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float...
#pracunittests
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
float someOrigin = 0.0f;
float...
#pracunittests
Opaque: Arrange-Act-Assert
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
flo...
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
flo...
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
flo...
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
flo...
#pracunittests
Opaque: Arrange-Act-Assert
void withDirection_Left_InvertsGradient() {
float someValueAtOrigin = 10.0f;
flo...
#pracunittests
The Opaque Anti-Pattern
#pracunittests
The Opaque Anti-Pattern
• Hard to see "how"?
#pracunittests
The Opaque Anti-Pattern
• Hard to see "how"?
• Demystify magic literals
#pracunittests
The Opaque Anti-Pattern
• Hard to see "how"?
• Demystify magic literals
• Consistent informative test name
#pracunittests
The Opaque Anti-Pattern
• Hard to see "how"?
• Demystify magic literals
• Consistent informative test name
...
#pracunittests
2/4: The Wet Anti-Pattern
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float)
#pracunittests
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)
2/4: The Wet Anti-Pattern
RacingLin...
#pracunittests
RacingLineOffsets.setSignedDistanceToRacingLine(RacingLine, float, int)
2/4: The Wet Anti-Pattern
> Test li...
#pracunittests
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffs...
#pracunittests
void getIdealOffset_BeyondRacingLineEdge_ReturnsEdge() {
float someRacingLineRadius = 10.0f;
float someOffs...
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius...
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius...
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius...
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius...
#pracunittests
Wet: Helper Functions
void getIdealOffset_WithinRacingLineEdge_ReturnsOffset() {
float someRacingLineRadius...
#pracunittests
The Wet Anti-Pattern
#pracunittests
The Wet Anti-Pattern
• Hard-to-maintain hacky tests?
#pracunittests
The Wet Anti-Pattern
• Hard-to-maintain hacky tests?
• Keep production sensibilities in unit test code
#pracunittests
The Wet Anti-Pattern
• Hard-to-maintain hacky tests?
• Keep production sensibilities in unit test code
• St...
#pracunittests
The Wet Anti-Pattern
• Hard-to-maintain hacky tests?
• Keep production sensibilities in unit test code
• St...
#pracunittests
3/4: The Deep Anti-Pattern
> Test failed:
> getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets
> With ...
#pracunittests
void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() {
VehicleID someOwnerID = (VehicleID)1;
Line...
#pracunittests
void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() {
VehicleID someOwnerID = (VehicleID)1;
Line...
#pracunittests
void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() {
VehicleID someOwnerID = (VehicleID)1;
Line...
#pracunittests
void getOwnerAtOffset_WithOwner_ReferencesOwnerAtManyOffsets() {
VehicleID someOwnerID = (VehicleID)1;
Line...
#pracunittests
Deep: One Assert Per Test
void getOwnerAtOffset_WithOwnerAndNegativeOffset_ReturnsOwner() {
float someNegat...
#pracunittests
> Test failed:
> getOwnerAtOffset_WithOwnerAndNegativeOffset_ReturnsOwner
> With Assert: VehicleID 0 != 1
>...
#pracunittests
The Deep Anti-Pattern
#pracunittests
The Deep Anti-Pattern
• Test failures not fully informative?
#pracunittests
The Deep Anti-Pattern
• Test failures not fully informative?
• Too many explicit assumptions per test
#pracunittests
The Deep Anti-Pattern
• Test failures not fully informative?
• Too many explicit assumptions per test
• Min...
#pracunittests
4/4: The Wide Anti-Pattern
#pracunittests
4/4: The Wide Anti-Pattern
> Executed 613 test(s), 599 test(s) passed, 14 test(s) failed.
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = n...
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = n...
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = n...
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = n...
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = n...
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = n...
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = n...
#pracunittests
// in DraftBehaviourFixture…
void updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = n...
#pracunittests
Wide: Seams
void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
Game Code
#pracunittests
Wide: Seams
void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
class HeatMap { virtual void W...
#pracunittests
Wide: Seams
void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
class HeatMap { virtual void W...
#pracunittests
Wide: Seams
void DraftBehaviour.updateImpl(WorldInfo wi, HeatMap heatInOut);
class HeatMap { virtual void W...
#pracunittests
Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo...
#pracunittests
Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo...
#pracunittests
Wide: MockHeatMapvoid updateImpl_WithCarToDraft_PushesAIToBehindCar() {
WorldInfo worldInfo = new WorldInfo...
#pracunittests
The Wide Anti-Pattern
#pracunittests
The Wide Anti-Pattern
• False-negative test failures?
#pracunittests
The Wide Anti-Pattern
• False-negative test failures?
• Many implicit assumptions
#pracunittests
The Wide Anti-Pattern
• False-negative test failures?
• Many implicit assumptions
• Isolate code with seams...
#pracunittests
Recap
#pracunittests
Recap
• Respect unit test source code as much as
production source code
#pracunittests
Recap
• Respect unit test source code as much as
production source code
• Write once, read many
#pracunittests
Recap
• Respect unit test source code as much as
production source code
• Write once, read many
• Only 1 ex...
#pracunittests
Recap
• Respect unit test source code as much as
production source code
• Write once, read many
• Only 1 ex...
#pracunittests
• andrew.fray@gmail.com
• @tenpn
• andrewfray.wordpress.com
• Roy Osherove: Art of Unit Testing
www.artofun...
Upcoming SlideShare
Loading in …5
×

Practical unit testing GDC 2014

4,884 views

Published on

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

Published in: Technology

Practical unit testing GDC 2014

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

×