The Role of Testing in Design

I'm a huge proponent of unit testing. At my current workplace, I try to be a proponent of more emphasis on testing, because well-tested code with well-written tests [1] is a pleasure to work with. I also feel that test-driven development (TDD) is a valuable tool in the right place.

When is TDD useful? It's great for adding functionality, especially business functionality, to existing code:

  1. Write a test that clearly documents the intended business function.
  2. Implement the business function.

Writing the test first often clarifies what the desired business behaviour should be, and the test clearly documents and verifies that desired behaviour.

However, TDD does not belong everywhere. TDD can be a barrier when designing an interface or API. To expand on this, lets agree on something first: well-written tests test functionality and interfaces; they do not test implementation. It should be possible to modify an implementation with minimal or no changes to a test suite.

Design is an iterative process, and the first one is rarely the best one. Writing tests against an interface that is under design makes it more effort to change that interface, and the earlier the tests are written, the less likely it is that the interface will be iterated on. This results in suboptimal designs.

A simple example illustrates the problem. Suppose you imagine a Worker class that you know consumes objects. So you create an interface: consume(Object toConsume). In a TDD methodology, one would write tests defining the fashion in which the consume function works. However, this is not necessarily the right API. It may turn out that your Worker more naturally consumes sequences of objects instead: consume(Seq[Object] toConsume). At best, all your tests must be refactored to accommodate this. Worse, you might not remove the first, incorrect, consume method at all, instead leaving unnecessary complication in your API (and in your test suite!). Writing tests too early has increased the amount of work and may even result in a weaker API.

A Better Way

What is a better workflow? In my experience:

  1. Prototype a design until you understand it well and it has the properties you desire.
  2. Write tests and flesh out implementation.

This is more efficient and may result in more well thought-out interfaces. Note that it's important to still keep testing in mind when designing, however; an un-testable design is not a good one. Once you are satisfied with the design, writing tests against it should prove that it is testable.

[1] On the other hand, well-tested code with poorly written tests is often resistant to refactoring.