Design for Testing (DfT): Why it matters (part one)

Learn the importance of design for testing and achieving higher testing maturity levels by incorporating sound design practices.

As teams embark on their journeys to establish higher testing maturity, it is important to take a step back and consider what that truly means. When Kent Beck pioneered Extreme Programming (XP), he cemented the fact that software testing is synonymous with software development. Despite our unanimous acceptance of this fact, the industry as a whole still has not fully adopted Test Driven Development (TDD) practices. Enterprise initiatives to achieve higher testing maturity levels are a call for us to engage in sound design practices that incorporate testing into development. In this article, and in a subsequent article, I will explain what we mean by “design for testing.”

Architectural design with cohesion and loose coupling in mind

This might seem like an odd place to start when the topic is “testing,” but I assure you these concepts are intertwined. A large part of any architectural design is to increase cohesion among software objects within a domain while decreasing coupling between domains. In fact, this is the main premise behind the Layered Architectures of “yester-years.” Nowadays, we recognize this old design approach by new names such as: Hexagonal, Clean, and Onion Architectures. The proponents for each had independently identified a hidden nuance from the original Layer Architecture: the separation between “inside” and “outside.”

Each of these new “concentric architectures,” introduces the concept of shielding your internal layers (use case and domain layers) from external dependencies. Actually, a “shield” isn’t the best analogy here. Consider a “backflow regulator” that you would have on your hose bibs. The regulator is there to ensure your water flows in only one direction. Similarly, you build a “regulator” between each layer to enforce a one-way dependency direction flow. 

Dependency flows inward (with example)

Without this slight shift in our mental model, we would draw our dependency arrows straight down through each horizontal layer, often mistakenly designating the bottom layer as a database or some other external data store. However, with our new concentric layering picture in mind, we can clearly see that external dependencies are on the outside of our application, and any dependencies we find going outward must be inverted.

Dependencies should flow inward

Flow of dependencies should point inwards

Dependency direction (with example)

An example illustrating dependency flows will help clarify this design principle. I find this example more compelling when depicted in a layered style. The diagram below illustrates a typical setup of objects spanning across a three layer architecture: a Rest Controller, a Service, a Business object, and a Database Storage object that interfaces with an external database. The external database interfacing responsibility places the Database Storage object squarely in the Interface Layer. This is different from many layered architectures drawn a decade ago where we would comfortably place the database in a fourth layer below the Domain Layer.

The dashed arrows (UML notation for dependencies) indicate dependency directions from the controller to the service and from the service to the business object. The business object relies on the storage object in the Interface Layer. This direct dependency violates our design principle, as indicated by the red dependency arrow pointing upwards. All dependency arrows should point downwards (or inwards when drawn with hexagons) towards the Domain Layer. This design principle aims to eliminate external dependencies from your business logic, thereby safeguarding your core application from external changes.

By the way, this is where TDD shines! Attempts to write tests for the business object will alert the developer to the unnatural tight coupling between the business object and the database storage object because the test will need to know about the database. This is when we refactor to correct the design.

Example of incorrect domain object dependency on Interface

Incorrect dependency direction for Domain objects to depend on Interface objects

Design correction (with an example of indirection)

To correct the design, we add a layer of indirection, so-called because the objective is to make internal layers indirectly dependent on higher layers. Additionally, the indirection allows us to invert the dependency by simply adding a Java interface. This achieves two desirable architectural characteristics. First, the coupling between the business domain and the database are loosened. Note that this refactoring is more than just a change to how the storage object is invoked. For instance, if the business object has to pass in a "where-clause" to the storage object, then we still have tight coupling between them.

The second architectural achievement is that the objects that make up each layer are now cohesive in their intent. Business objects should not need to have code concerning how data is being retrieved or saved.

Similarly, Services implementing a use case workflow do not need to know the minutiae of all the business rules or worry about interactions with external components. And Interface objects require only their expertise in handling the external systems being "plugged" into the application.

Adding the Storage interface allows the Database Storage object to implement the interface. The green "implementation" arrow, shown in the figure below, correctly points downward (and inward) towards the domain layer, satisfying the dependency design tenet.

Example of design correction by adding a layer of indirection

Correction of the design involves adding a layer of indirection

Astute developers would recognize that Spring conventions pretty much enforce this design tenet, since Spring leverages locations where indirection is used as targets for dependency injection. Also, the convenience of testing frameworks like Mockito allows for substituting test mocks in place of any Spring Bean. The convenience that Spring and Mockito frameworks provide add to a perceived irrelevance of this (and other) fundamental design principle. However, sound design choices go beyond just being able to write tests easily. For example, we might evaluate our designs by asking questions like: do the service and business objects know they are using a relational database? Can we update database drivers without considering their impact on core business logic? Can we easily switch out a relational store for a NoSQL store? In short, does the design provide freedom to evolve the system if desired? This is what TDD truly offers, and while one of its side effects is ensuring testing is done (i.e., code coverage), it is not the primary purpose.

Design for testing explained

We discussed how we design for testing and explained that TDD advocates for testing and development to be a single endeavor. We explored a common three-tier architecture and demonstrated how adding an abstraction can help invert dependency directions so that we establish the correct dependency flows between boundaries of different software concerns. In part 2 of this post, we will delve into breaking down testing into categories and determining which strategies should be applied to each.

Learn more about tech at Capital One

New to tech at Capital One?


David Wong, Distinguished Engineer, Card

David Wong is an architect on the Card team with extensive experience in Java applications and technical leadership gathered since the late 90s. His experience comes from a variety of sources including startups, consulting for federal agencies and large corporations like Capital One. One of his passions is to break down tough technical topics into a set of related simple concepts with helpful insights for clarity.

Related Content

Software Engineering
Article | January 8, 2024
Daniela Jorge
Article | November 14, 2023 |5 min read
Soft focus image of a sturdy metal chain in cool tones of blue and grey
Article | March 16, 2022 |5 min read