DDD & CQRS & Event Sourcing. Part 3: Test Domain Model

Before we start, I recommend checking my previous post where I described designing a domain model with events and which benefits it brings for business and engineers.

Let’s start with the basic class for domain model, let’s call it Aggregate (you could find more about this keyword in DDD here):

public abstract class Aggregate
{
public Guid Id { get; private set; }
private readonly IList<object> _events = new List<object>();

public ICollection<object> DequeueEvents()
{
    var events = _events.ToList();
    _events.Clear();
    return events;
}

private void Enqueue(object @event)
{
    _events.Add(@event);
}

protected void Process(object @event)
{
    Type thisType = GetType();
    if (thisType == null) throw new NotSupportedException($"Current this type is null!");
    
    MethodInfo methodInfo = thisType.GetMethod("Apply",  BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, new [] { @event.GetType()}, null );
    if (methodInfo == null) throw new NotSupportedException($"Missing handler for event {@event.GetType().Name}");
    
    try
    {
        methodInfo.Invoke(this, new[] { @event } );
    }
    catch(TargetInvocationException ex)
    {
        ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
    }
    Enqueue(@event);
}
}

As you could see, we have 3 main methods here:

  • Enqueue – simply add a new event to private event collection
  • Dequeue – returns the private event collection and clean the queue, so Aggregate event collection could not be changed in any way from outside.
  • Process – Try to find and call a proper method Apply (by method parameter type) and add anew event to the private event collection.

Also you could see private _events variable for storing events and Id property, which is common for every Aggregate object.

So our specific Aggregate will look like this:

public class User: Aggregate
{
public string Email { get; private set; }

public User(Guid id, string email) => Process(new Events.V1.UserCreated(userId:id, email: email ));

public void ChangeEmail(string userEmail) => Process(new Events.V1.UserEmailChanged(userId: Id, email: userEmail));

private void Apply(Events.V1.UserCreated @event)
{
    Id = @event.UserId;
    SetUserEmail(@event.Email);
}

private void Apply(Events.V1.UserEmailChanged @event) => SetUserEmail(@event.Email);

private void SetUserEmail(string email)
{
    email = email?.Trim();
    CheckNullOrEmpty(email, "Email");
    CheckMaxLength(50, email, "Email");
    CheckIsMatch(Constants.EmailTemplate, email, "Email");
    Email = email;
}
}

* For the simplicity of the example, I’ve omitted implementation details of methods CheckNullOrEmpty, CheckMaxLength, and CheckIsMatch. Anyway, if you are interested, you could find it in Github Repo here.

User domain model contains:

  • Email property (in addition to Id property from Aggregate class). Please be sure it has a private setter
  • SetUserEmail – a method for set and check if the email is correct
  • 2 private Apply methods for changing the internal state of the Domain Model
  • Public constructor
  • public ChangeEmail method for changing the email property

So when are speaking about the testing, we could check several things in the domain model:

  • It has a proper state – eg. User has not empty id and email
  • It generates a proper event – e.g. User generates UserCreated Event
  • Generated event has a proper state – UserCreated Event contains User Id and Email

Let’s see an example of such tests. I’m using Xunit, but you could use any test framework you like:

public class UserCreateTests
{
private readonly Domain.User _createdUser;
private readonly (Guid Id, string Password, string Email) _userData;

public UserCreateTests()
{
    _userData = (Id: Guid.NewGuid(), Password: "Testing123!", Email: "test@email.com");
}

[Fact]
public void ShouldBeCreatedWithCorrectData()
{
    Assert.NotNull(_createdUser);
    Assert.Equal(_userData.Id, _createdUser.Id);
    Assert.Equal(_userData.Email, _createdUser.Email);
}

[Fact]
public void ShouldGenerateUserCreatedEvent()
{
    var events = _createdUser.DequeueEvents();
    Assert.Single(events);
    Assert.Equal(typeof(Events.V1.UserCreated), events.Last().GetType());
}

[Fact]
public void UserCreatedEventShouldContainsCorrectData()
{
    var @event = (Events.V1.UserCreated) _createdUser.DequeueEvents().Last();
    Assert.Equal(_createdUser.Id, @event.UserId);
    Assert.Equal(_userData.Email, @event.Email);
}
}

Firstly, ShouldBeCreatedWithCorrectData checks if the User object is in a valid state after creation. Next, there are 2 tests for the events: ShouldGenerateUserCreatedEvent – checks if the event which has been generated has an expected type, and UserCreatedEventShouldContainsCorrectData checks if this event contains correct data.

As you could see, there is no big difference between unit testing regular objects, and those which are using Events for building their state. I hope this article helped you to understand better the philosophy of Event Sourcing. In the next post, I will describe the persistence of the aggregate using the Marten library. CU 🙂

The latest code base version of the project could be found on Github Org Page.