Unit Testing – Should Domain Services Be Mocked?

cdomain-driven-designunit testing

I am trying to decide whether to introduce mocks in my isolated Domain Model tests. I have a class method similar to this:

public Offer AssignOffer(OfferType offerType, IOfferValueCalculator valueCalculator) 
        { 
            DateTime dateExpiring = offerType.CalculateExpirationDate(); 
            int value = valueCalculator.CalculateValue(this, offerType); 
            var offer = new Offer(this, offerType, dateExpiring, value); 
            _assignedOffers.Add(offer); 
            NumberOfActiveOffers++; 
            return offer; 
        } 

which I took from here: https://github.com/jbogard/presentations/blob/master/WickedDomainModels/After/Model/Member.cs

I have now read this article: http://enterprisecraftsmanship.com/2016/06/15/pragmatic-unit-testing/ and this article: http://www.taimila.com/blog/ddd-and-testing-strategy/. They both seem to suggest that I should not mock OfferType (as it is a Value Object). However my question is: should I be mocking IOfferValueCalculator (a Domain Service)? IOfferValueCalculator does not sit in the innermost layer of the Onion, however it does sit in the Domain Model (second most inner layer of the Onion).

The reason I ask is because all these articles specifically reference Entities and Value Objects (advising against mocking them), however they do not reference Domain Services.

Best Answer

Should I mock a Domain Service?

Short answers

  1. It depends.
  2. Providing test doubles that stand in for domain services will often be a good idea.
  3. Providing mocks that stand in for domain services may be a good idea.

What follows will make more sense if you are familiar with the different flavors of test doubles; Mocks Aren't Stubs is an accessible starting point.

Longer answers:

Using test doubles is a trade off. There are risks associated with the fact that the system under test isn't talking to a real collaborator, and there are benefits. Part of our craft is understanding the trade we are making.

There are properties that we want our tests to have.

  • We want the tests to be fast, so that running them is less of a distraction.
  • We want the tests to be isolated, so that we can run them in parallel, and save even more wall clock time.
  • We want the tests to be simple -- fewer lines of code in the test suite means fewer bugs in the test suite
  • We want the tests to be comprehensible -- we want most of the code to be describing the test, rather than describing a bunch of scaffolding
  • We want the tests to be stable -- if we don't make any changes, a second run of a test should always give us the same result as the first
  • We want the tests to be accurate -- a failing test should always indicate a mistake.

But all of those are just bikewash unless

  • The tests call our attention to mistakes that we make.

Replacing real collaborators (which tend to be messy) with fake collaborators (which tend to be simple) increases the probability that we miss certain categories of mistakes; so we had better be sure when we do that that the benefits we gain offset the increased risks.

We derive almost no additional benefit from mocking a value. Well designed value objects are already well isolated, side effect free, and tend to express the semantics of a test better than a substitute would. They live entirely within the functional core of your application.

If you run that same math on entities, you will see that it doesn't make much sense to mock an entity either.

With domain services, however, the trade offs start to look really interesting. Domain services are the mechanism by which an encapsulated part of the domain model communicates with its collaborators; those collaborators might be other parts of the same model, or they may be further away.

When Evans described domain services in the blue book, he included among the motivating examples needing to access application and infrastructure services -- code that lives outside of the abstraction boundary of the domain model. Domain services are often proxies for communicating side effects, which may even cross process boundaries.

Mock across architecturally significant boundaries, but not within those boundaries.

Domain services are often proxies for code across an architecturally significant boundary.

So if you've got a domain service that is an in memory abstraction -- Orders needs access to an in memory tax calculator service, then the test double doesn't provide nearly as much marginal benefit as a test double that stands in for a domain service that needs to talk to a database....

Put another way, there are a lot of different things under the umbrella term "domain service", and they have different trade offs.

You are much more likely to use a test double when the actual behavior of the service is hard to predict, or hard to constrain.

IOfferCalculator.CalculateValue is a simple method in this case - it does not connect to a database and it does not call any other methods.

That sounds like the marginal advantage of introducing a mock is small; so I would recommend using the real implementation in this circumstance.