We develop some features by creating a rich domain model or Aggregate. This is a graph of entities, with a root, properties with private setters and public methods for operations on it.
This aggregate encapsulates the business logic that allows to change its state. Also, these aggregates can raise events, which basically mean they are placed in a collection for later dispatch from the unit of work (@jbogard I believe I took this from one of your blog posts).
This setup allows us to unit test this logic quite nicely.
With all the logic encapsulated in the aggregates, the handlers mostly take the form of:
- load aggregate from DB (using a unit of work)
- call a method on the aggregate
- store to DB (unitOfWork.Commit)
some times with some extra logic that cannot be embedded in the aggregate for different reasons.
The problem appears when we want to unit test the handlers.
It is very easy to mock the unit of work, to return a specific instance of an aggregate and to assert that the Commit method is called.
The problem is that it’s not straight forward to assert if or which aggregate method was called. The only way we have right now is to assert that the state of the aggregate has changed as expected (looking at some properties). The problem with this approach though is that it leaks information from the aggregate unit tests into handlers unit tests, therefore if we ever change the aggregate we also break a lot of handler unit tests.
The only solution I’ve come up with, that doesn’t imply a big pattern change is to create an interface for each aggregate with the public methods. This would allow to test the handlers with a mock of the aggregate instead of the real implementation (or a proxy of the real implementation). But it feels a bit like forcing something into the domain model just for testing purposes.
What are your thoughts?
You could also make the method on the aggregate virtual instead of using an interface. Then you can derive from it in the test and assert it was called but it has the same smell that you mentioned. For me writing tests is a risk mitigation thing and there is no black or white. So if the risk is high of breaking something I’d live with the caveats and introduce the interface or virtual method. If the risk is low I would not even bother testing that handler part (just pair review them) and focus on this part only.
If you have logic inside your handlers that are complex enough for you to want to unit test it, perhaps an option would be to move this logic into a domain service or application service (depending on the nature of the logic).
Let me try adding a bit more to the @danielmarbach comment. It might be worthwhile to answer the following questions:
- What part of the system cares about changes to the aggregates?
- How are those changes queried/published/visible to that part of the system?
- Can those mechanisms be reused in the tests?
- Are all changes equal or are some of those of more/less importance?
- In terms of risk/value mentioned by @danielmarbach it could be valuable to ask if the handlers should be tested or if handlers should be used in the test in addition to other infrastructure?
I know that I’m giving questions instead of answers but I find those kind on questions useful when working on software myself. I hope those will be of use to you as well.
Thanks all for your input.
Regarding the type of logic that we have in handlers, it’s normally not too complex and I agree that the complex parts should be encapsulated in another class testable by itself, but we would still want to test the integration of the unit of work, the aggregate and that extra class and this can only happen at handler level.
Other scenarios are:
- the handler decides whether to load an existing aggregate or create a new one.
- the handler decides whether to call an external service or not
- the handler decides whether to commit the changes or not (maybe the external service failed…)
- the handler uses the NSB context (sends a command, a response, etc)
So, it not about complex logic, it’s about asserting that a few things that need to happen really happen.
From a technical point of view, @danielmarbach’s suggestion of making the methods virtual is much better than the interface option. It’s a bit intrusive in the model, but it’s much lighter than the interface. And having worked with NHibernate for a while (a long time ago) it won’t hurt me too much
@tmasternak I appreciate the questions and thoughts and I’ll take them into consideration in the testing effort we are doing right now.