Pragmatic way to unit test your DDD tactical patterns using both the unit testing school of thought and the famous test pyramid

Normand Bédard
5 min readMay 17, 2023

--

Over the past years, a lot of businesses have started to embrace the power of Domain-Driven Design in their software development practices.

At the same time, testing practices continued to evolve. In my opinion, one of the most remarkable topics in unit testing is the Classical vs London school of thought. The London school of thought considers the “isolated part” of unit testing as a single class, using mocks for all collaborators. This is why it is also known as the Mockist school of thought. From what I have seen in the field in the last 12 years, this is the way that developers learn to unit test their code. But the industry learned over time that this approach leads to fragile tests needing a lot of maintenance. That is the issue that the Classical school of thought solves by considering the “isolated part” of unit testing as a full piece of functionality, using mocks only for external shared and unmanaged dependencies like API calls, database access or filesystem access.

Like any tool or school of thought, nothing is a silver bullet. Some approaches can be preferable to others under some circumstances. And when it comes to applying unit testing strategies in a DDD code base, I think the flexibility of choosing the right school of thought for the different DDD tactical patterns is a big plus value.

Everybody knows about the test pyramid about unit / integration / end-to-end tests. But what about reusing this pyramid image to build a unit test strategy for the different DDD tactical patterns?

Value Objects are powerful tools. Their lifecycle (immutability) makes them easy to deal with, and they avoid side effects like bypassing aggregate root invariants when you expose them outside the aggregate. However, people often forget they are more than simple DTOs. Value objects are particularly good candidates to own business rules too; people could be incredibly surprised at which point it is possible to move business rules from Services or Entities to Value Objects. In that context, Value Objects can benefit naturally from the London school of thought because of their simplicity. Also, since Value Objects can be reused across a system in other aggregate roots, they benefit from a large test coverage.

Aggregate Roots are a key element of DDD. They enforce invariants (business rules) with an atomic data integrity boundary. Ideally, the public contract of the Aggregate Root should not change if you refactor the internal structure of the aggregate. The goal is to avoid large ripple effects in the code base and on your unit tests. Anyway, you should never expose your Entities outside the Aggregate Root, otherwise you would be able to bypass the invariants the Aggregate Root is trying to protect. So do not hesitate to expose dedicated DTOs from your Aggregate Root. They can be seen as an ACL (kind-of …) between the internal structure and the public contract of your aggregate root. In that context, the Classical school of though is the better approach for unit testing. Refactoring the internal structure of the aggregate will not have any impact on your tests: they will still compile and only assert the public unchanged contract of your Aggregate Roots (observable state). And this is impossible with the London approach. In my opinion, Aggregate Roots should be at the heart of your testing strategy. No compromise here. Even if test coverage percentage should not be an end goal, the responsibility of the Aggregate Roots is so high that a low-test coverage is probably a sign that you are not testing them properly because they are at the center of your domain and should handle a lot of invariants.

Domain Services are a bit tricky. They are the last place where invariants should be handled because they lead to anemic domain model and low object-oriented code base. They are tricky because when an invariant is hard to handle in an Aggregate Root inside Entities or Value Objects (or because they involve multiple aggregate roots), people tend to put them by default in Domain Services. The reason behind this is because it is easier to do that compared to challenging and refactoring your aggregate root boundaries, even if it is generally a better solution. In that context, you should not have a lot of tests in that pyramid layer. The opposite would be an indicator of an anemic domain model. But in the end, since they can include invariants, you must be sure to test them.

The last layer of the pyramid is the Application Services. Those services should never include business rules. Their goal is to handle infrastructure concerns like unit of work, data access using repositories, high level steps orchestration of business processes and things like that. They should delegate the application of business rules to Aggregate Roots (or as a last resort, to Domain Services). Tests on this layer should focus on happy paths only. If you try to validate each invariant of every single Value Objects / Aggregate Roots / Domain Services from that layer, you will end up with a lot of complexity in your test setup phase (which reduces code readability) and you will tend to have a lot of invariant test duplication. The same way invariants should never be duplicated in the production code, it is a good habit to not test those multiple times for no valuable reason. This is the pyramid test layer that benefits the most from the Classical school of thought because their test footprint is huge and because you do not want to refactor them each time you refactor your production code. But be careful, it is also the easiest place to create tests without value by simply asserting high level steps orchestration. That is why they are at the top of the pyramid. They cover a lot of code, but they should be limited in number. Not because they take more time to run, but because they should not focus on all individual invariants already tested in other pyramid layers.

As you can see, interesting associations can be made between DDD, unit testing and an adapted test pyramid. I believe it gives good guidance on how to unit test your tactical DDD patterns in a pragmatic way without sacrificing quality assurance and by allowing sustainable software development practices. Each tactical pattern has its own responsibility and importance, and your test strategy should reflect that.

--

--

Normand Bédard
Normand Bédard

Written by Normand Bédard

French Canadian senior software developer for SherWeb since 2010. Ultramarathon, drones and camping enthusiast!

No responses yet