It has been six months since I started working at ClimatePartner building a new enterprise application. My new team follows some agile practices such as pair programming and Test-Driven Development (TDD), and I have the honest feeling that the sun is shining for us!
There are some issues that I have been facing both now and in past industrial experiences on the topic of building solid enterprise applications that I would like to explain to you in this article.
Furthermore, I will also propose a simple test-first-based methodology to build enterprise applications that enhance team communication and promote faster high-quality code deliveries.
Without further ado, let's get to it!
Agile practices are highly beneficial for the fast prototyping of software. TDD sits at the core of such practices, providing software with a paramount property: robustness. Writing tests upfront forces developers to think about the expected and exceptional behaviour of the software components they build, enhancing code quality and ensuring the fulfilment of functional requirements.
While TDD is no silver bullet as one must comply with good practices such as the SOLID principles, it is a powerful practice that enables developers not to be afraid to fix, clean, and adapt their code to functional requirement updates. And that is all great. However, it is not that easy to effectively apply TDD.
At the beginning of the construction of any new enterprise application, we (developers) gather several functional requirements. Then, we derive a set of use cases that will satisfy such functional requirements. After that, we develop software components sitting at different layers, from the highest to the lowest, drilling down to the heart of the application i.e., its domain model. That is the definition of the top-down software development process.
However, the bottom-up software development process fits better with TDD. Compared to the top-down alternative, bottom-up is a more pragmatic approach, since it enables us to incrementally cover all levels of indirection starting with the most basic (i.e., the domain model) and gradually moving towards higher layers of abstraction. It allows the production of sounder application code foundations, which in turn makes us gain great confidence in our work. To apply TDD in a top-down approach, however, one should first write tests for software components located at the higher layers. This way, developers don’t need to mock any dependencies to any lower-layer components, since they simply do not exist just yet.
The need to create such dependencies is already a problem because mocking lower layer components is not always possible or, in the best case scenario, feels counter-intuitive e.g., imagine yourself having to mock the logic of a domain object for the sake of producing a service component test.
Furthermore, I personally doubt that it would bring any value at all, as I think that intermediate component validation should always exercise all dependencies except those to external services (e.g., a database), which can be mocked. More importantly, due to the inherent complexity to realise non-trivial functional requirements as code, one does not fully understand the technical implications that some given functional requirements have on the domain model until one starts reasoning about them during the implementation of the domain model.
Once again, starting to write tests of intermediate software components does not bring much value, since many of those tests (if not all) are likely to be thrown to the dustbin once the lower layer software artefacts are actually implemented.
Furthermore, I have seen software developers (especially junior teammates) giving up and implementing some proof of concept for the use case at hand without writing any validation logic whatsoever. This is using the code-first practice, which defeats the purpose of TDD. Also, without following proper Continuous Delivery practices, there is a high risk of ending up pushing non-validated code to our version control repository.
So, how could we effectively apply TDD in the production of enterprise applications given a set of functional requirements?
There is plenty of literature on Hexagonal Architecture on the Internet. I would especially recommend reading the white paper on the topic written by Alistair Cockburn.
For the purpose of the present article, please allow me to tell you a real-life short story aimed to briefly explain the motivation and main benefits of Hexagonal Architecture: In the many years that I have been developing enterprise applications I have seen many people (myself included) starting new projects focusing on other topics rather than in our real mission. Such a mission consists of providing actual value to the companies we work for. That value is on the domain logic of our applications.
Paraphrasing Uncle Bob in his book Clean Architecture, everything else is a distraction, an implementation detail that can (and should) be postponed, ideally to the end of development. Examples of implementation details are database technologies, controller logic, or frontend technology. Even the backend framework is an implementation detail that we could pick later in the development process if we really wanted. Hexagonal Architecture, also called Ports and Adapters, is an architectural pattern aimed to decouple software application core logic from outer implementation details.
We developers should focus on the core logic of enterprise applications and postpone the implementation of the logic required to communicate with external services. To achieve that goal, all we really need to do is to write some interfaces (so-called ports) and mock the components (so-called adapters) that actually communicate with the external services. Thus, adapter implementation can come later in the development process. And the later they come, the better, since all the insights that we gain while we produce the core logic proves really useful during the decision-making of which technologies to pick.
Consider the elements that compose the inner hexagon. Apart from the domain model, there is also a layer of application services. These software components do not specify any domain logic. Instead, they are simple coordinators of adapter and domain model logic. An application service realises a use case that takes care of a subset of functional requirements for the enterprise application. This is important data to keep in mind for what comes next.
As I stated earlier, applying TDD is easier when following the bottom-up software development process. However, many of us find it easier to reason about a system design following the top-down approach. And although it seems that we are incurring a conflict of interest, that is fine because we can start designing (i.e., sketching in pseudocode or some UML diagram) our application services top-down without writing a single line of code for them; not until we complete the implementation of the domain model.
Before we start coding, we could interpret application services as some software design guidelines to perform vertical slices of the enterprise applications we are to build. Each vertical slice represents the whole execution path of a use case, from the action performed by an upstream external service or a user in a UI to any operation executed on a downstream external service. By the time we finish with the design of an application service, we have identified which adapters and domain components we need to implement. Now that we have visibility on the domain model, we can next implement its fundamental components by applying TDD.
Then, we can implement the application service following the test-first approach, creating a port to any external service adapter and mocking its actual implementation. By the time we are done with the implementation of the application service and the related domain model, we can ascertain that such implementation is valid i.e., likely to be bug-free and matching its functional requirements. Finally, we can implement the adapter logic, also applying TDD.
This methodology enables fast and gradual implementation of enterprise applications, allowing developers to gain confidence in the validity of all the components they create without throwing any test away. Moreover, it does not impose any restrictions on functional requirement updates.
This diagram depicts the methodology to write the logic of one use case. Notice that I do not talk about specific test types since this is a pretty controversial subject, although I would recommend following the conventions and terminology used in The Practical Test Pyramid article.
The proposed methodology simplifies team task distribution, no matter whether they work alone or in pairs. Some of its steps can be done in parallel e.g., a teammate/pair can build the core logic mocking any reference to an external dependency, whereas another can be working on the development of the integration code with such external dependency, since these two parts are decoupled, thus shortening delivery time. All they need to do is convey some API for the external dependency realised as a port. Both the mock and the actual external dependency need to implement the aforementioned port interface. Similarly, another team/teammate/pair can implement the frontend part for the use case, if that is what the enterprise application demands.
On another hand, due to its structured nature, communicating the state of development of a concrete use case among peers is easy. When handing over a use case development, the departing entity can simply point the new one to the current application service they were designing or implementing. The new entity then can simply trace anything that has been already created on adapter or domain logic in the code base.
A final note on implementation details: We could update our domain model to specify some of those details by writing annotations/decorators of the database technology and input data validation resources from the underlying framework that adjusts best to the nature of our application. I would however advise against it, as that would be leaking implementation details into our domain model, which is not the best practice since implementation details and the domain model tend to change for different reasons and frequency. On another hand, as explained by Vaughn Vernon in his book Implementing Domain Driven Design (DDD), Hexagonal Architecture is closely related to DDD. However, you do not need to follow the set of DDD practices and patterns to build complex enterprise applications that are based on Hexagonal Architecture, although I would highly recommend you do. But those decisions, after all, are entirely up to you.
Test-Driven Development is a powerful practice in the construction of robust enterprise applications. TDD is best applied following the bottom-up software development process. However, because developers are used to reasoning about such applications following a top-down approach, applying it in practice is challenging. This article exposes a simple methodology to help developers effectively apply TDD in the development of enterprise applications.