Testability and Design
Testability and Refactoring
The need to test has an impact on the two most fundamental aspects of good object-oriented design, cohesion and coupling. The existence of tests has an impact on another important area related to design: the ability to accommodate new changes over time.
In fact, the most significant indicator of a quality design, cohesion and coupling aside, is its ability to support change over time. A design is as good as the next unforeseen requirement it must support. Reality states that change is continual in the vast majority of systems.
Too much complexity has at least two devastating impacts: Comprehension time increases dramatically, thus increasing maintenance costs, and overall development slows, because it's more costly to create all of these abstractions.
The second possible solution is to keep the solution as simple as possible. Introduce abstractions only as needed, but ensure that redundancies are kept to a minimum, and ensure that the system retains high levels of expressiveness. These goals result in a system that minimizes complexity and keeps maintenance costs lower.
Neither solution is perfect. Developers will never be perfect at keeping a system clean enough, nor will they be perfect enough to ensure that a design is as abstract as it could possibly be. The result is that there are always going to be new features that cost more than they should have (in other words, were developers to have incorporated such features in an initial, comprehensive design). Given the two options, I believe the choice of the simple design is far more preferable.
To sustain a simple design (or even the highly abstract design), enough tests must be kept in place to ensure its correctness. Keeping a design simple is a continual challenge, because every line introduced into a system has the potential of increasing the system's complexity. Constant vigilance is essential, and appropriate efforts require that code be cleansed with the introduction of every few lines of code. Such tests must run fast, if they are to be run often enough to be useful.
To summarize: The existence of tests enables refactoring, which allows a design to stay clean enough over time to accommodate new requirements cheaply.
Testability and Correctness
A system could exhibit the most beautiful possible design on paper—high levels of cohesion, low levels of coupling, appropriate abstractions, no violations of the law of Demeter, appropriate use of design patterns, appropriate naming of classes, methods, and fields, and so on. All of that design effort is worthless if the program does not behave as expected. The only way to know that a program behaves as expected is through testing.
Arguments about acceptable percentages of code coverage should be met with the realization that the percentage representing lack of code coverage is a measure of potential broken-ness. If 70% of the system is covered (whether it be by unit tests, acceptance tests, integration tests, or other tests), 30% of the system could be completely broken.
To summarize: Having tests ensures that the system isn't worthless. A worthless system can have the worst possible design—we don't care!
About the Author
Jeff Langr is a veteran software developer celebrating his 25th year of professional software development. He's authored two books and dozens of published articles on software development, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him via email at jeff at langrsoft dot com.
Page 2 of 2