Testability and Design

Wednesday Oct 10th 2007 by Jeff Langr

How important is testing in software development? Explore the relationship between testability and design.

Timing is everything. Write tests first, and you can get as good levels of unit test coverage as anyone could expect. Write tests after, and, well, you probably won't write nearly as many tests. That's only one reason to chose test-driven development (TDD) over test-after development (TAD). In this article, I'll discuss where testability and design are aligned with one another and where they are not.

Timing is indeed everything. I wrote about TDD vs. TAD two weeks ago. In the interrim, Michael Feathers wrote a blog entry entitled "The Deep Synergy Between Testability and Good Design." I think it's the right time to bang on this point, and hard: Lack of concern for testability is why most of our systems are poorly constructed at best.

Feathers points to the essence of how testability relates to design by talking about cohesion and coupling. These "OO-101" concepts are still the most important indicators of a good design.

Testability and Cohesion

What's cohesion, and why does it good? According to Wikipedia, cohesion is "measure of how strongly-related and focused the various responsibilities of a software module are." Cohesive modules are easy to comprehend. Cohesive modules are more reusable. A module that does only one thing (and does it well) is more likely to provide value in a different context than a module that aggregates many behaviors.

How does testability relate to cohesion? Feathers points to the fact that private methods often hint that a class is encapsulating multiple responsibilities. Yet private methods present some challenging questions: how do they get tested, and should they be (directly) tested?

Of course, private methods get tested as long as the public methods that call them get tested. But many developers often feel a compulsion to find away to test private methods more directly. Some languages provide cheats: In Java, private methods can be tested through reflection, and in C++, test classes can be designated as friends.

The more common cheat, however, is to loosen access from private to something more public. It's a cheat, because it goes against strong desires to maintain encapsulation. Per many developers, you just don't violate private protections for anything. On rare occasion, I'll use this cheat—to me, it's far more important to know that my code is actually working. That's an example of the tradeoffs that can exist between testability and design.

More frequently, the better solution is to create a new class, and move the behavior that was previouly private to the public side of the new class. This falls in line with the high cohesion goal. It also means that the behavior is easy to test. In this predominance of cases where a private methods belongs elsewhere, testability aligns with good design.

As a simple example, here are a couple methods from a class named Holding:

public Date dateDue() {
   return addDays(dateCheckedOut, getHoldingPeriod());

private Date addDays(Date date, int days) {
   Calendar calendar = Calendar.getInstance();
   final long msInDay = 1000L * 60 * 60 * 24;
   calendar.setTime(new Date(date.getTime() + msInDay * days));
   calendar.set(Calendar.HOUR, 0);
   calendar.set(Calendar.MINUTE, 0);
   calendar.set(Calendar.SECOND, 0);
   calendar.set(Calendar.MILLISECOND, 0);
   return calendar.getTime();

The needs of the method addDays are important enough, and eough logic is in play, that it should be exhaustively tested. Does it work if days is 0? Does it support negative numbers for days? Not only do I want to know that it works correctly, but I want the answers to these questions documented. So, I'm compelled to write tests. I could write these tests against dateDue, but the questions they answer have little to do with the dateDue calculation.

The addDays method is private. If I take the approach of moving it to another class, perhaps named DateUtil, the method is easily tested. It becomes a general-purpose method, and promotes the notion of reuse. The method move reduces the amount of "iceberg" code—encapsulated code hidden within non-public methods. Code in Holding becomes less cluttered as a result.

To summarize: The insistence on testing drives the interest in cohesive modules, because they are much easier to test.

Testability and Coupling

What's coupling, and why is lots of it bad? According to Wikipedia, coupling is "the degree to which each program module relies on each one of the other modules." Coupling is essential in any system—modules that don't talk to each other don't produce much of a system. But, lots of coupling can lead to other problems, such as when a change in one portion of the system breaks something in another part of the system, or the ripple effect, where a change forces changes to many other modules. And as with low cohesion, high coupling minimizes opportunities for reuse: A module coupled to many others simply can't be used in many contexts.

Tightly-coupled modules are hard to test. To test a class that has many collaborators, each of which in turn have many collaborators, often requires considerable setup. If Holding depends on Book, which depends on Catalog, which in turn depends upon something else, the test will likely have to create and properly populate instances of each of these types of objects.

Extensive intra-system coupling often leads to rampant coupling to external dependencies, such as database or other API calls. External dependencies are particularly troublesome for testing. Setting up external configurations is often difficult and something that requires continual diligence.

The need to test promotes the development of systems where interfaces are introduced in order to support stubs and mocks. These interfaces act as walls of abstraction, helping minimize tight coupling to implementation details that are likely to change. The need to test also promotes a different approach to design, one that minimizes long dependency chains in the first place.

To summarize: The insistence on testing drives the interest in decoupled modules, because they are much easier to test.

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.

There are two ways to design a system so that it easily accommodates new features. One way is to keep the design as generic as possible. For example, a generic design might eliminate every possible conditional via a polymorphic hierarchy, and reduce every concrete notion to a number of abstractions. I've actually seen this attempted in a database design, where the well-meaning designer replaced the simple notion of a customer table with a half-dozen tables. (I recall table names like Entity, Entity-Person, and Entity-Aggregator.) The system was incomprehensible to everyone but its designer.

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.

Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved