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:

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.