Chapter 4: Testing

Test Driven Development

Suppose that we followed three simple rules –

  1. Don’t write any production code until you have written a failing unit test.
  2. Don’t write more of a unit test than is sufficient to fail or fail to compile.
  3. Don’t write any more production code than is sufficient to pass the test.

If we worked this way, we’d be working in very short cycles. We’d be writing just enough of a unit test to make it fail and then just enough production code to make it pass. We’d be alternating between these steps every minute or two.

The first and the most obvious effect is that every single function of the program has tests that verify its operation. We can add functions to the program or change the structure of the program without fear that in the process, we will break something important. The tests tell us that the program is still behaving properly.

The act of writing the tests first forces us into a different point of view. We must view the program we are about to write from the vantage point of the caller of that program. Thus, we are immediately concerned with the interface of the program as well as its function. By writing the test first, we design the software to be conveniently callable.

What’s more, by writing the test first, we force ourselves to design the program to be testable. In order to design a software to be callable and testable, the software has to be decoupled from its surroundings. Thus, the act of writing tests first forces us to decouple the software.

Another important effect of writing tests first is that the tests act as an invaluable form of documentation. The tests act as a suite of examples that help other programmers figure out how to work with the code. This documentation is compilable and executable. It will stay current. It cannot lie.

The art of writing the tests first, and then making the failing test pass (by writing the code that confirmed to the structure implied by the test) is called intentional programming. You state your intent in a test before you implement it, making your intent as simple and readable as possible. You trust that this simplicity and clarity points to a good structure for the program.

The act of writing the tests first is an act of discerning between design decisions.

Serendipitous Decoupling

The need to isolate the module under test forces us to decouple in ways that are beneficial to the overall structure of the program. Writing tests before code improves our design.

Acceptance Tests
Unit tests are white box tests (a test that knows and depends on the internal structure of the module being tested) that verify the individual mechanisms of the system. Acceptance tests are black box tests (a test that does not know and depend on the internal structure of the module being tested) that verify that the customer requirements are being met.

Acceptance tests are automated. They are the ultimate documentation of a feature. Once the customer has written the acceptance tests that verify that a feature is correct, the programmers can read those acceptance tests to truly understand the feature. So, just as unit tests serve as compilable and executable documentation for the internals of the system, acceptance tests serve as compilable and executable documentation of the features of the system. In short, the acceptance tests become the true requirements document.

Serendipitous Architecture

Note that the acceptance tests place a pressure on the architecture of the system. The very fact that we consider TDD leads us to the notion of an API for the functions of the system.

The most important benefit of all the testing is the impact it has on architecture and design. To make a module or an application testable, it must also be decoupled. The more testable it is, the more decoupled it is. The act of considering comprehensive acceptance and unit tests has a profoundly positive effect on the structure of the software.

Leave a comment