Testing on the Toilet: Test Behavior, Not Implementation
Monday, August 05, 2013
By Andrew Trenk
This article was adapted from a Google Testing on the Toilet (TotT) episode. You can download a printer-friendly version of this TotT episode and post it in your office.
Your trusty Calculator class is one of your most popular open source projects, with many happy users:
You also have tests to help ensure that it works properly:
However, a fancy new library promises several orders of magnitude speedup in your code if you use it in place of the addition operator. You excitedly change your code to use this library:
That was easy, but what do you do about the tests for this code? None of the existing tests should need to change since you only changed the code's implementation, but its user-facing behavior didn't change. In most cases, tests should focus on testing your code's public API, and your code's implementation details shouldn't need to be exposed to tests.
Tests that are independent of implementation details are easier to maintain since they don't need to be changed each time you make a change to the implementation. They're also easier to understand since they basically act as code samples that show all the different ways your class's methods can be used, so even someone who's not familiar with the implementation should usually be able to read through the tests to understand how to use the class.
There are many cases where you do want to test implementation details (e.g. you want to ensure that your implementation reads from a cache instead of from a datastore), but this should be less common since in most cases your tests should be independent of your implementation.
Note that test setup may need to change if the implementation changes (e.g. if you change your class to take a new dependency in its constructor, the test needs to pass in this dependency when it creates the class), but the actual test itself typically shouldn't need to change if the code's user-facing behavior doesn't change.
This article was adapted from a Google Testing on the Toilet (TotT) episode. You can download a printer-friendly version of this TotT episode and post it in your office.
Your trusty Calculator class is one of your most popular open source projects, with many happy users:
public class Calculator { public int add(int a, int b) { return a + b; } }
You also have tests to help ensure that it works properly:
public void testAdd() { assertEquals(3, calculator.add(2, 1)); assertEquals(2, calculator.add(2, 0)); assertEquals(1, calculator.add(2, -1)); }
However, a fancy new library promises several orders of magnitude speedup in your code if you use it in place of the addition operator. You excitedly change your code to use this library:
public class Calculator { private AdderFactory adderFactory; public Calculator(AdderFactor adderFactory) { this.adderFactory = adderFactory; } public int add(int a, int b) { Adder adder = adderFactory.createAdder(); ReturnValue returnValue = adder.compute(new Number(a), new Number(b)); return returnValue.convertToInteger(); } }
That was easy, but what do you do about the tests for this code? None of the existing tests should need to change since you only changed the code's implementation, but its user-facing behavior didn't change. In most cases, tests should focus on testing your code's public API, and your code's implementation details shouldn't need to be exposed to tests.
Tests that are independent of implementation details are easier to maintain since they don't need to be changed each time you make a change to the implementation. They're also easier to understand since they basically act as code samples that show all the different ways your class's methods can be used, so even someone who's not familiar with the implementation should usually be able to read through the tests to understand how to use the class.
There are many cases where you do want to test implementation details (e.g. you want to ensure that your implementation reads from a cache instead of from a datastore), but this should be less common since in most cases your tests should be independent of your implementation.
Note that test setup may need to change if the implementation changes (e.g. if you change your class to take a new dependency in its constructor, the test needs to pass in this dependency when it creates the class), but the actual test itself typically shouldn't need to change if the code's user-facing behavior doesn't change.
This actually fits nice to a posting by Jakub Holy "Principles for Creating Maintainable and Evolvable Tests" (2011-11-30): http://bit.ly/17wHKyK
ReplyDeleteAnd my post on Testing Pyramid, Code Coverage and using acceptance tests - even on Unit-Test-Level: http://bit.ly/17wHSOZ
This could also be extended to UI level tests (Selenium, mobile), where UI element changes should not affect tests unless the user/system flow changes (drastically).
ReplyDeleteOf course, in order to measure whether you have achieved the promised "several orders of magnitude speedup" you'd also need to have performance tests & a benchmark of your current implementation.
ReplyDeleteIn the last paragraph, you mentioned: "Note that test setup may need to change if the implementation changes (e.g. if you change your class to take a new dependency in its constructor, the test needs to pass in this dependency when it creates the class), but the actual test itself typically shouldn't need to change if the code's user-facing behavior doesn't change".
ReplyDeleteIsn't this the case for the example above? Calculator takes additional AdderFactor in the constructor and so the existing tests just need to change the setup code. But I belive the actual call to add() doesn't need to be changed in the tests. So the example above is actually acceptable and not demonstrating your point