DDD & Unit Tests

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 how to use the typical domain-driven design structure to help:

  • What should I test?
  • How can I make my code easy to unit test?

Why Are Unit Tests Important?

Isn’t writing all those unit tests slowing you 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?

Nope.

Let’s look at some examples. What if you…

  • Don’t wait long enough for your bread to rise?
  • Drive too fast and miss the sign for your destination?
  • Run “all-out” at the beginning of a marathon?
  • Skip through a book without stopping and thinking about what you are reading?

Building Software Too Fast

Building software too fast can cause 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 your software development life cycle early can have many benefits:

  • Forces you to think about requirements more rigorously
  • Helps you approach designing your 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 taking it a bit easier at the beginning of 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.

domain expert translation

By having more understanding of the domain, you can embed that knowledge as tests in your code.

This implies that your code must somehow match how the business works.

Focus On Behaviours

When you focus not on “how should we structure our database tables”, etc., but on the behaviour of the domain, your testing becomes much easier. And, the requirements become explicitly embedded in your code – not by chance.

A Simple Example

Imagine you are working in an e-commerce domain. You have behaviour in the domain where an order can only be placed if it has items in the shopping cart.

If you code against this requirement explicitly, then you 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 this:

var emptyCart = GetEmptyShoppingCart();
var order = new Order(emptyCart);    
Assert.IsFalse(order.CanPlaceOrder);

You might not have started with this kind of design in your mind. 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!

Appendix: Domain-Events

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.

domain-driven design testing

When using domain events, you can isolate our tests to examine the behavior of one aggregate, for example. This will make your tests smaller, more focused and therefore more useful.

To test the first step in this example, you 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. You 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).