I could spend pages discussing architectural patterns that enable testability. Instead, I will focus on what I consider the most important advice: separate infrastructure code from domain code.
The domain is where the core of the system lies: that is, where all the business rules, logic, entities, services, and similar elements reside. Entities like Invoice
and services such as ChristmasDiscount
are examples of domain classes. Infrastructure relates to all code that handles an external dependency: for example, pieces of code that handle database queries (in this case, the database is an external dependency) or web service calls or file reads and writes. In our previous examples, all of our data access objects (DAOs) are part of the infrastructure code.
In practice, when domain code and infrastructure code are mixed, the system becomes harder to test. You should separate them as much as possible so the infrastructure does not get in the way of testing. Let’s start with InvoiceFilter
example, now containing the SQL logic instead of depending on a DAO.
Listing 7.1 InvoiceFilter
that mixes domain and infrastructure
public class InvoiceFilter {
private List<Invoice> all() { ❶
try {
Connection connection =
DriverManager.getConnection("db", "root", ""); ❷
PreparedStatement ps =
connection.prepareStatement("select * from invoice"));
Result rs = ps.executeQuery();
List<Invoice> allInvoices = new ArrayList<>();
while (rs.next()) {
allInvoices.add(new Invoice(
rs.getString("name"), rs.getInt("value")));
}
ps.close();
connection.close();
return allInvoices;
} catch(Exception e) { ❸
// handle the exception
}
}
}
public List<Invoice> lowValueInvoices() { ❹
List<Invoice> issuedInvoices = all();
return issuedInvoices.all().stream()
.filter(invoice -> invoice.value < 100)
.collect(toList());
}
}
❶ This method gets all the invoices directly from the database. Note that it resides in the InvoiceFilter class, unlike in previous examples.
❷ JDBC code to execute a simple SELECT query. If you are not a Java developer, there is no need to know what PreparedStatement and Result are.
❸ Database APIs often throw exceptions that we need to handle.
❹ The same lowValueInvoices method we’ve seen before, but now it calls a method in the same class to get the invoices from the database.
We can make the following observations about this class:
- Domain code and infrastructure code are mixed. This means we will not be able to avoid database access when testing the low-value invoices rule. How would you stub the private method while exercising the public method? Because we cannot easily stub the database part, we must consider it when writing the tests. As we have seen many times already, this is more complex.
- The more responsibilities, the more complexity, and the more chances for bugs. Classes that are less cohesive contain more code. More code means more opportunities for bugs. This example class may have bugs related to SQL and the business logic, for example. Empirical research shows that longer methods and classes are more prone to defects (see the 2006 paper by Shatnawi and Li).
Infrastructure is not the only external influence our code may suffer from. User interfaces are often mixed with domain code, which is usually a bad idea for testability. You should not need the user interface to exercise your system’s business rules.
Besides the hassle of handling infrastructure when writing tests, extra cognitive effort is often required to engineer the test cases. Speaking from experience, it is much easier to test a class that has a single responsibility and no infrastructure than it is to test a non-cohesive class that handles business rules and, for example, database access. Simpler code also has fewer possibilities and corner cases to see and explore. On the other hand, the more complex the code is, or the more responsibilities it has, the more we must think about test cases and possible interactions between features that are implemented in one place. In the example, the interaction between the infrastructure code and the business rule is simple: the method returns invoices from the database. But classes that do more complex things and handle more complex infrastructure can quickly become a nightmare during testing and maintenance.
The architecture of the software system under development needs to enforce a clear separation of responsibilities. The simplest way to describe it is by explaining the Ports and Adapters (or Hexagonal Architecture) pattern. As Alistair Cockburn proposed (2005), the domain (business logic) depends on ports rather than directly on the infrastructure. These ports are interfaces that define what the infrastructure can do and enable the application to get information from or send information to something else. They are completely separated from the implementation of the infrastructure. On the other hand, the adapters are very close to the infrastructure. They are the implementations of the ports that talk to the database, web service, and so on. They know how the infrastructure works and how to communicate with it.
Figure 7.1 An illustration of the Hexagonal Architecture (or Ports and Adapters) pattern
Figure 7.1 illustrates a hexagonal architecture. The inside of the hexagon represents the application and all its business logic. The code is related to the application’s business logic and functional requirements. It knows nothing about external systems or required infrastructure. However, the application will require information or interaction with the external world at some point. For that, the application does not interact directly with the external system: instead, it communicates with a port. The port should be agnostic of the technology and, from the application’s perspective, abstract away details of how communication happens. Finally, the adapter is coupled to the external infrastructure. The adapter knows how to send or retrieve messages from the external infrastructure and sends them back to the application in the format defined by the port.
Let’s cook up a simple example that illustrates these concepts in practice. Suppose an online web shop has the following requirements:
For all the shopping carts that were paid today, the system should
- Set the status of the shopping cart as ready for delivery, and persist its new state in the database.
- Notify the delivery center, and let them know they should send the goods to the customer.
- Notify the SAP system.
- Send an e-mail to the customer confirming that the payment was successful. The e-mail should contain an estimate of when delivery will happen. The information is available via the delivery center API.
The first step is identifying what belongs to the application (the hexagon) and what does not. It is clear that any business rule related to ShoppingCart
, such as changing its state, as well as the entire workflow the shopping cart goes through once it’s paid, belongs inside the hexagon. However, a service that provides e-mail capabilities, a service that communicates with the SAP, a service that communicates with the delivery center API (which is probably offered as a web service), and a service that can communicate with the database are all handled by external systems. For those, we need to devise a clear interface for the application to communicate with (the ports) together with a concrete implementation that can handle communication with the external system (the adapters). Figure 7.2 illustrates the concrete application of the Ports and Adapters pattern to this example.
Figure 7.2 A concrete implementation of the Hexagonal Architecture (or Ports and Adapters) pattern for the shopping carts example
A natural implementation for the PaidShoppingCartsBatch
class would be the code in listing 7.2. It does not contain a single detail regarding infrastructure. This entire class could easily be unit-tested if we stubbed its dependencies. Does it need a list of paid shopping carts, normally returned by cartsPaidToday()
? We stub it. Does it notify the SAP via the cartReadyForDelivery()
method? We mock SAP
and later assert the interaction with this method.
When we put everything together in production, the method will communicate with databases and web services. But at unit testing time, we do not care about that. The same testing philosophy we discussed applies here: when (unit) testing the PaidShoppingCartsBatch
class, we should focus on PaidShoppingCartsBatch
and not its dependencies. This is possible here because (1) we receive its dependencies via the constructor (which enables us to pass mocks and stubs to the class), and (2) this class is only about business and has no lines of infrastructure code.
Listing 7.2 PaidShoppingCartsBatch
implementation
public class PaidShoppingCartsBatch {
private ShoppingCartRepository db;
private DeliveryCenter deliveryCenter;
private CustomerNotifier notifier;
private SAP sap;
public PaidShoppingCartsBatch(ShoppingCartRepository db,
➥ DeliveryCenter deliveryCenter,
CustomerNotifier notifier, SAP sap) { ❶
this.db = db;
this.deliveryCenter = deliveryCenter;
this.notifier = notifier;
this.sap = sap;
}
public void processAll() {
List<ShoppingCart> paidShoppingCarts = db.cartsPaidToday();
for (ShoppingCart cart : paidShoppingCarts) { ❷
LocalDate estimatedDayOfDelivery = deliveryCenter.deliver(cart); ❸
cart.markAsReadyForDelivery(estimatedDayOfDelivery); ❹
db.persist(cart); ❹
notifier.sendEstimatedDeliveryNotification(cart); ❺
sap.cartReadyForDelivery(cart); ❻
}
}
}
❶ All dependencies are injected, which means we can pass stubs and mocks during testing.
❸ … notify the delivery system about the delivery
❹ … mark it as ready for delivery and persist that to the database
❺ … send a notification to the customer
Look at the class’s four dependencies: ShoppingCartRepository
, DeliveryCenter
, CustomerNotifier
, and SAP
. These are interfaces and, in the Hexagonal Architecture, ports. They establish a protocol for communication between the application and the external world. These interfaces are completely agnostic of technology and infrastructure details. In other words, they abstract all the complexity of the infrastructure away from the domain code. As a result, the interfaces do not depend on anything strange, such as database or web service classes. They do depend on other domain classes, such as ShoppingCart
, and that is fine. The following listing contains the interface declarations of all the ports.
Listing 7.3 Interface declarations of all the ports
public interface DeliveryCenter { ❶
LocalDate deliver(ShoppingCart cart);
}
public interface CustomerNotifier { ❷
void sendEstimatedDeliveryNotification(ShoppingCart cart);
}
public interface SAP {
void cartReadyForDelivery(ShoppingCart cart);
}
public interface ShoppingCartRepository { ❸
List<ShoppingCart> cartsPaidToday();
void persist(ShoppingCart cart);
}
❶ The DeliveryCenter interface’s concrete implementation will probably consume a very complex web service, but the port abstracts this away. Ports speak business language and do not let infrastructure details leak.
❷ The same thing happens for CustomerNotifier and all other interfaces/ports.
❸ This one does not even have “database” in the name. “Repository” is a more business-like term.
We are now only missing the implementation of the adapters. This code is out of the scope but in terms of implementation, these adapters are classes that implement the ports’ interfaces. The next listing provides some skeleton code to give you an idea what the adapters will look like.
Listing 7.4 Simplified implementation of the adapters
public class DeliveryCenterRestApi implements DeliveryCenter {
@Override
public LocalDate deliver(ShoppingCart cart) {
// all the code required to communicate
// with the delivery API
// and returns a LocalDate
}
}
public class SMTPCustomerNotifier implements CustomerNotifier {
@Override
public void sendEstimatedDeliveryNotification(ShoppingCart cart) {
// all the required code to
// send an email via SMTP
}
}
public class SAPSoapWebService implements SAP {
@Override
public void cartReadyForDelivery(ShoppingCart cart) {
// all the code required to send the
// cart to SAP's SOAP web service
}
}
public class ShoppingCartHibernateDao
implements ShoppingCartRepository {
@Override
public List<ShoppingCart> cartsPaidToday() {
// a Hibernate query to get the list of all
// invoices that were paid today
}
@Override
public void persist(ShoppingCart cart) {
// a hibernate code to persist the cart
// in the database
}
}
Why does this pattern improve testability? If our domain classes depend only on ports, we can easily exercise all the behavior of the domain logic by stubbing and mocking the ports. In the PaidShoppingCartsBatch
example, we can stub and mock the ShoppingCartRepository
, DeliveryCenter
, CustomerNotifier
, and SAP
ports and focus on testing the main behavior of the PaidShoppingCartsBatch
class. Again, we do not care if the DeliveryCenter
adapter does its job properly. That one will be exercised in its own test suite.
Listing 7.5 shows an example test of PaidShoppingCartsBatch
. This is a single test. As a developer, you should apply all the testing techniques and devise several test cases for any behavior and corner cases you see. Even exceptional behaviors can be easily exercised.
Listing 7.5 Test for PaidShoppingCartsBatchTest
, mocking the ports
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) ❶
public class PaidShoppingCartsBatchTest {
@Mock ShoppingCartRepository db;
@Mock private DeliveryCenter deliveryCenter;
@Mock private CustomerNotifier notifier;
@Mock private SAP sap;
@Test
void theWholeProcessHappens() {
PaidShoppingCartsBatch batch = new PaidShoppingCartsBatch(db,
➥ deliveryCenter, notifier, sap); ❷
ShoppingCart someCart = spy(new ShoppingCart()); ❸
LocalDate someDate = LocalDate.now();
when(db.cartsPaidToday()).thenReturn(Arrays.asList(someCart));
when(deliveryCenter.deliver(someCart)).thenReturn(someDate);
batch.processAll();
verify(deliveryCenter).deliver(someCart); ❹
verify(notifier).sendEstimatedDeliveryNotification(someCart);
verify(db).persist(someCart);
verify(sap).cartReadyForDelivery(someCart);
verify(someCart).markAsReadyForDelivery(someDate);
}
}
❶ The @ExtendWith and @Mock annotations are extensions provided by Mockito. We do not even need to write Mockito.mock(…). The framework instantiates a mock for us in these fields.
❷ Instantiates the class under test and passes the mocks as dependencies
❸ The ShoppingCart is a simple entity, so we do not need to mock it. Nevertheless, let’s spy on it to assert its interactions later.
❹ Verifies that interactions with the dependencies happened as expected
Although we only tested the application code, the code from the adapters should also be tested. The real implementation of the ShoppingCartRepository
—let’s call it ShoppingCartHibernateDao
(because it uses the Hibernate framework)—will contain SQL queries that are complex and prone to bugs, so it deserves a dedicated test suite. The real SAPSoapWebService
class will have complex code to call the SOAP-like web service and should also be exercised. Those classes require integration testing, following our discussion of the testing pyramid. I show how to write some of those integration tests.
NOTE Although I could also have mocked the ShoppingCart
class, I followed the advice do not mock entities unless they are complex. I preferred to spy on them rather than mock them.
This idea of separating infrastructure from domain code appears not only in Cockburn’s Hexagonal Architecture but also in many other interesting works on software design, such as the well-known Domain-Driven Design by Evans (2004) and Martin’s Clean Architecture (2018). This principle is pervasive among those who talk about software design and testability. I agree with all these authors.
A common question for those new to the Hexagonal Architecture (or domain-driven design, or clean architecture) is, “Do I need to create interfaces for every port?” I hope to convince you that there are no rights and wrongs, that everything depends, and that being pragmatic is key. Of course you do not have to create interfaces for everything in your software system. I create interfaces for ports where I see more than one implementation. Even if I do not create an interface to represent an abstract behavior, I make sure the concrete implementation does not leak any of its implementation details. Context and pragmatism are kings.
To sum up, the main “design for testability” principle I follow at the architectural level is to separate infrastructure from business code. Do not be tempted to think, for instance, “This is a simple call to the database. Look how easy it is to implement here!” It is always easier to write untestable code, but doing so will bite you in the future.
Leave a Reply