Reputation: 920
I am trying to learn some of the ideas of DDD and Clean Architecture and I came across a problem: how do I unit test Command Handlers that live in the Application Layer, verifying they call the right methods on domain objects, without testing the logic within those domain objects? Let's say I have the following domain entity:
public class User
{
public User(int id)
{
Id = id;
}
public int Id { get; }
public void RemoveProfilePicture()
{
...
}
...
}
And I have the following simple Command class:
public class RemoveUserProfilePictureCommand : ICommand
{
public RemoveUserProfilePictureCommand(int userId)
{
UserId = userId;
}
public int UserId { get; }
}
And a Command Handler (located in the Application Layer):
public class Handler : ICommandHandler<RemoveUserProfilePictureCommand>
{
private readonly IUserRepository userRepository;
public Handler(IUserRepository userRepository)
{
this.userRepository = userRepository;
}
public void Handle(RemoveUserProfilePictureCommand command)
{
var user = userRepository.GetById(command.UserId);
user.RemoveProfilePicture();
}
}
I would like to verify that a call to Handle
will find the right User
and call the domain method RemoveProfilePicture
on it. The only two options I found do not satisfy me and I am looking for a better solution.
The obvious solution is to assert that the action on the domain model occurred, something like:
[Fact]
public void Handle_UserExists_ShouldRemoveProfilePicture()
{
var user = new User(id: 555);
repository.GetById(user.Id).Returns(user);
var command = new RemoveUserProfilePictureCommand(user.Id);
handler.Handle(command);
Assert.Null(user.ProfilePicture);
}
The problem I have with this approach is that we make the assertions based on the logic inside the domain model, and if that logic was more complex than setting the ProfilePicture
property to null
we would still have to assert its results in the tests of the Command Handler, even though the domain logic is already covered by its own unit tests. The problem arises from the tight coupling of the Application Layer classes to the Domain classes. Which led me to the second solution:
If the User
class would implement an interface, say IUser
, then the fake repository in the test could return a different implementation of IUser
to the Command Handler that verifies the correct method was called. The problem here is that from my understanding, the Application Layer should be a thin wrapper around the domain and should not be decoupled from it. Also, all the examples I found always used concrete types for Domain Objects, and it does seem odd to implement an interface in an entity class.
Can anybody see a better solution for testing such classes? because it's not about some edge case, it's about almost every Command Handler class, and most of them are more complex than the simple example I gave above.
Upvotes: 5
Views: 1625
Reputation: 3413
I had the exact same question when I was doing a DDD inspired CQRS project as well. From what I gather, you are also trying to develop a CQRS project since you are using concepts such as commands and are adamant about returning a null
value in your command handler.
With that said, asserting a null
value is not testing anything. What you are trying to test is that a side-effect has occurred in the system. In your case, you want to ensure that "a user profile picture has been removed".
What you seem to be missing is the other half of CQRS: events. When a command handler succeeds with its transaction (i.e. side-effect), you would typically dispatch an event that would represent that. Your unit tests would then simply assert that a UserProfilePictureRemoved
event was dispatched (or enqueued, depending on your implementation).
Upvotes: 1
Reputation: 1069
In my opinion the idea of DDD is to use the same logic in implementation as is used in business. Make it explicit, make it visible and avoid the logical mismatch between the business and the software.
What is a command? It is an intent to change the state of a domain (data). An intent, that can be either accepted and executed, or rejected.
Acceptance and rejection is making up a decision. It can be implemented as a pure logic, in a functional way. It may be quite complex logic (think about a loan application, for example). But testing a pure logic is straightforward. This logic obviously belongs to the domain. By "straightforward" I mean that pure logic doesn't have any side effects and given the same inputs always produces the same outputs. So, testing is table-based. You just call it with some arguments and assert an output.
But then you need to prepare the data for making up a decision. And next you should execute it. This is an application logic.
By preparing the data I mean going to a database, to a storage, to configuration parameters, to external services. Most likely you need to know the current state of the domain. You need authorisation details. You need to know any additional details that could affect the decision. All this happens at the component boundary. It is not complex, mostly just accessing the read models, so you just mock integrations and verify that they are called with the correct parameters.
Executing a decision also happens at the boundary. It means updating the domain data and, maybe, notifying some third parties. Testing approach is the same as for the data preparation step. You mock an integration and verify parameters.
Of course, an integration implementations should be tested separately. Like SQL queries, HTTP interactions, reading and writing files... But that was not a part of your question, I suppose.
The difficulty with DDD is that it is hard to demonstrate on a simplistic example. In such cases it is most likely not needed. If you just update an image - what is the domain logic then? On the other hand, if you have an experience in some business domain - it would be quite easy to come up with a realistic example. But if you don't know the domain - usually any use case looks like CRUD.
From the top of my head... Think about about registering registering a real estate purchase contract. As an input you have a suit of documents. Then a specialist integrates with some systems to make sure that all the data in the documents is valid. It then checks if the property is eligible for the contract. Then a decision is the right of ownership. Or a rejection. Next, when decision is made up, all the necessary records in the state registries are made, keys transferred and so on...
You can ask then, what's the difference between such DDD and business processes that are run by business process engines? The difference is mostly about the timespan and a number of decision-making steps. In DDD all the flow happens within hundreds of milliseconds. There is a single decision-making step. No intermediate persisted state - all is stored in memory. While a business process may take days and weeks. And may involve multiple decision-making, data-preparation, execution and conditional branching.
Upvotes: 0
Reputation: 13256
My approach is use stated based testing on the domain classes, as you have done.
When it gets to the integration/application concerns I typically use interaction based testing and I'm not concerned with the state unless it assists in determining whether the interaction passed. Instead of checking the state I would ask my mocking framework whether a particular call had been made.
For truly simple scenarios as you have shown I don't think I'd bother though. Should I be specifically required to do so for the sake of code coverage then I probably would but for my own projects I probably would not :)
Upvotes: 0
Reputation: 57257
public void Handle(RemoveUserProfilePictureCommand command)
{
var user = userRepository.GetById(command.UserId);
user.RemoveProfilePicture();
}
My preferred answer: test it by having a second reviewer sign off on the claim that "the code is so simple there are obviously no deficiencies", then leave it alone. Seriously, how many times are you expecting to refactor these two lines? How many of those refactorings won't be automatic?
Second possibility is to add another layer of indirection; instead of having the handler work directly with the repository and the entity, instead work one layer out, expressing the protocol in terms of things that you can easily mock. So you have a wrapper around the repository, and the wrapper fetches the model's user from the "real repository" and wraps that user in another mockable object.
In effect, this part of the code treats the domain model as though it were an external dependency... which of course it is (from the perspective of the handler).
It's a trade: you get to keep the domain model closed, while reducing the amount of untested interesting code, but the number of pieces required goes up. There's also some risk of going overboard, which can lead to "test driven design damage".
The extra layer of indirection can work for you or against you when the API of the domain model is unstable.
There are lots of other variations of "add more stuff", but the trade-offs are effectively unchanged: you get more coverage, complexity goes up, you increase maintenance risks.
Option #3: As you noted, you can test the changes to the domain model. You end up with a "sociable" test instead of a "solitary" test.
One variation that sometimes helps to keep the tests stable is to ensure that the assertions are expressed in the calculus of the domain model itself. In effect, you have one user entity that acts as the control, and the other is your measurement, and then you ask the domain model to compare the two.
But again, notice that we are introducing more complicating elements to cover some really obvious code.
Upvotes: 0