March 9, 2021
Hot Topics:

Testability and Design

  • By Jeff Langr
  • Send Email »
  • More Articles »

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.

Page 1 of 2

This article was originally published on October 10, 2007

Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Thanks for your registration, follow us on our social networks to keep up-to-date