Let's consider the below example:
internal class Meeting
{
public int Id { get; set; }
}
internal class DailyRoomReservation
{
private ISet<Meeting> _meetings { get; set; } = new HashSet<Meeting>();
internal void ScheduleMeeting(Meeting meeting)
{
if (_meetings.Contains(meeting)) throw new InvalidOperationException();
_meetings.Add(meeting);
}
}
Assuming that DailyRoomReservation is my aggregate root (I intentionally ommited most of business logic for simplicity's sake), how should I test this? It is known good practise to expose only command methods for aggregates (CQS terminology), especially when using CQRS in big picture. Furthermore I have no business need for exposing the _meetings property (testing purpose of course is not a good reason to do this). I wrote following tests:
[Test]
internal void ScheduleNewMeeting_ShouldSucceed()
{
var uniqueMeeting = new Meeting() { Id = 1};
var dailyRoomReservation = new DailyRoomReservation();
dailyRoomReservation.ScheduleMeeting(uniqueMeeting);
}
[Test]
internal void ScheduleSameMeetingTwice_ShouldFail()
{
var meeting = new Meeting() { Id = 1};
var dailyRoomReservation = new DailyRoomReservation();
dailyRoomReservation.ScheduleMeeting(meeting);
Action scheduleMeeting = () => dailyRoomReservation.ScheduleMeeting(meeting);
scheduleMeeting.Should().ThrowExactly<InvalidOperationException>();
}
And they work pretty well, however I still cannot validate at that point whether the meeting has been really added. How can I refine my approach?
CodePudding user response:
Heuristic: write-only domain entities don't actually deliver any business value.
If you are putting information into a domain entity, you are doing that with the expectation that there may be some change to the information that comes out of it.
Thus, the basic structure of our test is that we obtain an instance of an entity, we send a sequence of commands to it, and we measure the information that comes out. Our domain entity passes the test if the measurement conforms to some predetermined specification (in other words, if the assert passes).
There are at least four different ways that we can get at the information that comes out.
First, we can actually reach in and look at the internal data model. There are contexts in which this is fine (typically disposable tests, aka scaffolding, that we are not expecting to double as documentation).
Second, we can query the entity for information - perfectly suitable when you have an entity that is expected to support that query as part of its representation of the domain.
Third, we can eavesdrop on the messages this entity sends to other parts of our code, and capture a copy of the information that way. This is a common approach in tell-dont-ask designs, where we use a test double/substitute to capture the information we want to evaluate.
Fourth, in cases where we expect durable storage of our domain data, we can "store" the entity and then either (a) examine its persisted representation or (b) load that representation into a "read model" and query that.
In a context where testing and observability are considered first class concerns, we would of course implement a query method to allow access to a copy of the information we want to verify. The fact that the implementation of our entity includes such a method doesn't necessarily mean that the method is part of the published interface.
In this particular example, you can use as your measurement "does it throw?" I would expect to see a minimum of four tests
- Adding one meeting does not throw
- Adding two meetings with different identifiers does not throw
- Adding the same meeting twice does throw
- Adding two meetings with the same identifier does throw
But in a domain where we want to treat the scheduling of a duplicate meeting as a no-op, this measurement isn't satisfactory, and we would instead need to write our test to detect some other flavor of variance.
