0

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?

Lepruz
  • 31
  • 5
  • Even if it's not exposed, `_meeting` must have some effect other than just existing. What is it used for? Could that provide a way to test it? Or can you expose it as a read-only collection? – Scott Hannen Jan 07 '22 at 17:46
  • Yep, it is read by my system several times, but entrypoints for all these operations are queries (as I mentioned, I tried to apply CQRS at architecture level). It can be tested by sending command that adds a meeting and then retrieve saved data via query. But it is possible only on low-level with black-box approach (hence not unit-tested). – Lepruz Jan 07 '22 at 18:01
  • May I ask how you are exposing the aggregates state to your persistence layer? – Andreas Hütter Jan 07 '22 at 18:57
  • What do you mean by exposing? How I allow an ORM to read it's state? if yes - I use Entity Framework and string-navigated property: `builder.Entity().HasMany("_meetings")` – Lepruz Jan 08 '22 at 23:19

1 Answers1

0

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.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
  • Thank you so much for your answer, but in order to better understand I'd like to show you examples that came to my mind for each of the types presented by you. **First** - do you mean looking into internal data model by using reflection or exposing its properties just for testing purpose? **Second**- it'll be done then on low-level (e.g. module testing?) **Third** -So assuming that each agregate root has ```DomainEvents``` property, I can test if it contains e.g. `MeetingScheduledEvent`? **Fourth** - I cannot spot the difference between this one and second type? – Lepruz Jan 08 '22 at 18:54