Home
Blog
Contact
Resume for Steven E. Newton
Papers
Readings for Code Janitors
Services
Steven E. Newton
Crater Moon Development
© 2003-2023
Unit Tests Manage Complexity
Unit testing is more than a design tool, more than a practice for enhances quality, it is a tool for managing complexity.
A proper, well-written unit test isolates a particular piece of behavior, and the code implementing it, from all the unnecessary surrounding context of the running system. The unit test focuses on an aspect of the code in by freeing the programmer from the need to build and maintain a complex mental picture of the running system. A programmer doesn’t have to “hold it all in her head”. Where before a programmer might have had to slowly build up a detailed mental model of the executing logic in order to understand the complex behavior sufficiently to (manually) evaluate and compare the results of a test run with the expected behavior, a good unit test narrows the scope of what the programmer must comprehend to something simple to evaluate. Simple enough to evaluate quickly and repeatedly, as often as necessary, automatically.
This narrow and simple focus makes it easer to achieve and recover “flow”, and less costly to lose it because of interruption. Maintaining flow becomes so easy, two programmers can work together in a combined state of flow.
The unit defined by a test represents a locally self-contained element of simple behavior. Something too hard to test is not simple enough. The programmer needs to remove excess behavior from the context until the code being tested represents a unified conceptual whole.
In my early days as a programmer, the usual process consisted of
- thinking a while about the problem until I had a notion about how an implementation might look.
- writing a bunch of code with what seemed like a correct solution at a high level
- fixing syntax errors until the code compiled
- fixing bugs until the code produced the correct output.
- dropping down to a lower level and repeating.
I called this “top-down design”.
As the code grew and became more detailed, starting up the system and running through the steps to verify the latest changes meant keeping more and more context in my head. This context includes things like what I changed most recently, what this run was supposed to have working, and what might break. It was work to get into flow, it was not easy to maintain, and it was easy for distractions to break flow.
Test-driven development breaks the work down into tiny chunks of independent function. Tiny chunks don’t require the programmer to maintain a complex mental context of logic.
Before TDD: A programmer had to keep a complex logical context in a mental model. With TDD: The programmer only keeps a simple context of making the next test pass.
Before TDD: Build and run the entire system, manually check the behavior, set debug breakpoints, and try to remember what was being worked on. With TDD: Build the piece being worked on, run the one failing test, have the behavior checked automatically, understand why the test fails. Do something simple to make the test pass. Repeat.