It’s common for software developers to struggle with creating useful unit tests. In my experience, many developers either don’t understand what to test and how to design testable code.
In this article, we’ll look at why I think domain-driven design can help with both of these issues:
- What should I test?
- How can I make my code easy to unit test?
Why Are Unit Tests Important?
Doesn’t having to write all those unit tests slow us down?
Isn’t it more work to test?
Yes. It is more work. It is slower.
But, that doesn’t mean it’s not worth investing in.
Does slow always mean worse?
Let’s look at some examples. What if we…
- Don’t wait long enough for our bread to rise? 😮
- Drive too fast and miss the sign for our destination? 🤪
- Run “all-out” at the beginning of a marathon? 👀
- Skip through a book without stopping and thinking about what we are reading? 🥺
Building Software Too Fast
Building software can face those kinds of issues too. When we move too fast we can run into issues:
- Lack of early product feedback
- Missed a bug while reviewing code
- Misunderstood what the true requirements of your customer were
- Hastily written code that will accumulate technical debt
I’m sure you can think of more 😉.
Incorporating testing into our software development life cycle early can have many benefits:
- Forces us to think about requirements more rigorously
- Helps us approach designing our code in a more modular way
- Presents a safety-net for future refactoring
And, if you haven’t experienced it, testing can actually make you more productive – just like a marathon 😉.
How DDD Helps Make Unit Testing Easier
Understanding True Requirements
One of the huge benefits of a domain-driven approach to creating software is that there’s a focus on understanding what the needs of the business domain really are.
DDD places an emphasis on minimizing the translation between business people (domain experts) and software developers.
By having more understanding of the domain, we can embed that knowledge as tests in our code.
This implies that our code must somehow match how the business works.
Focus On Behaviours
When we focus not on “how should we structure our database tables”, etc., but on the behaviour of the domain, our testing becomes much easier. And, the requirements become explicitly embedded in our code – not by chance.
A Simple Example
Imagine we are working in an e-commerce domain. We have behaviour in the domain where an order can only be placed if it has items in the shopping cart.
If we code against this requirement explicitly, then we should focus the test not on fetching from the database, etc. but on the interaction of the domain concepts (the order & shopping cart).
In this case, your unit test might look like:
var emptyCart = GetEmptyShoppingCart(); var order = new Order(emptyCart); Assert.IsFalse(order.CanPlaceOrder);
We might not have started with this kind of design in our minds. However, when you think about the domain objects in this way, there’s a clear benefit to how expressive and simple to test they can be!
There are times when you have behaviour that spans multiple bounded contexts or perhaps even multiple aggregates.
In these cases, the naive approach is to test the entire collaboration between aggregates, for example.
When using domain-events, we can isolate our tests to examine the behavior of one aggregate, for example. This will make our tests smaller, more focused and therefore more useful.
To test the first step in this example, we could do something as simple as:
var shoppingCart = GetShoppingCart(); var order = new Order(shoppingCart); order.PlaceOrder(); var @event = DomainEvents.GetRaised<OrderPlaced>(); Assert.IsEqual(order.OrderId, @event.OrderId);
This is straightforward and ensures the expected behaviour.
There’s no need to muck around with the order. We just need to know that it emitted the proper event.
For the other steps in the process, we might create separate unit tests for each one (which might exist in another bounded context).