Skip to content

Testing your Domain when Event Sourcing

Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


How do you test your domain when Event Sourcing? I find testing aggregates with Event Sourcing to be simpler than testing if you’re storing the current state. The inputs to your aggregate are events and the output of your aggregate are events.

Given a stream of events
When a valid command is performed
Then new event(s) occurs

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Event Sourcing

I’ve covered Event Sourcing in my Event Sourcing Example & Explained in plain English post. So check that out if you need a more detailed primer.

Generally, I use Aggregates as a way to model my domain. The aggregate is what exposes commands and if those commands are called/invoked will result in creating new events. This general concept is how testing in event sourcing becomes simple.

In order to use an aggregate, you first need to pull all the existing events from the Event Store, replay them within the Aggregate to get to the current state, then return the Aggregate to the caller. To do this, I generally use a Repository that will do this work and build the Aggregate.

To illustrate this, we have client code that is using the repository to get the Aggregate. The repository calls the Event Store to get the existing Events (if any) for this specific aggregate.

Testing your Domain when Event Sourcing

At this point the Repository will create an empty aggregate and replay all the events it received from the Event Store.

Testing your Domain when Event Sourcing

Once it has rebuilt the aggregate, it then returns it back to the Client. The client at this point will likely call various methods/commands on the aggregate.

This was the first stage of the process. I call this the rehydrating stage. You’re rehydrating your aggregate back to its current state with all the past events. Remember this first stage as the “Given” as we start testing in event sourcing.

Creating Events

The rest of the example is using a Warehouse Product, which is the concept of a product that is in a warehouse.

Now that the client code has an aggregate, it will likely perform one or more commands on the aggregate.

If the client calls ShipProduct() command on the aggregate, and the aggregate is in a valid state to do so and passes all invariants, then it will create a new event that it will keep internally within it.

If the client code then called another command, another event would be appended to the internal list.

This is the second stage of the process where we’ve created new events which are the result of calling commands on our aggregate. Remember this second stage as the “When” stage of testing with event sourcing.

Saving Events

The last step is taking the newly created events that are in the aggregate and persisting those to the event store.

Testing your Domain when Event Sourcing

This means the client code will call back to our Repository passing it the Aggreagate.

Testing your Domain when Event Sourcing

The repository will get the new events and append those to the Event Store for that specific aggregates event stream.

Remember this stage as the “Then” in our tests when Event Sourcing.

Given, When, Then

If you take that basic 3 steps of loading an aggregate, calling commands, saving the new events, you can boil that down to:

Given a stream of events
When a valid command is performed
Then new event(s) occurs

You can use this as the testing strategy for testing your Aggregates.

using System;
using System.Collections.Generic;
namespace EventSourcing.Demo
{
public class WarehouseProductState
{
public int QuantityOnHand { get; set; }
}
public class WarehouseProduct : AggregateRoot
{
public string Sku { get; }
private readonly WarehouseProductState _warehouseProductState = new();
public WarehouseProduct(string sku)
{
Sku = sku;
}
public override void Load(IEnumerable<IEvent> events)
{
foreach (var evnt in events)
{
Apply(evnt as dynamic);
}
}
public static WarehouseProduct Load(string sku, IEnumerable<IEvent> events)
{
var warehouseProduct = new WarehouseProduct(sku);
warehouseProduct.Load(events);
return warehouseProduct;
}
public void ShipProduct(int quantity)
{
if (quantity > _warehouseProductState.QuantityOnHand)
{
throw new InvalidDomainException("Cannot Ship to a negative Quantity on Hand.");
}
var productShipped = new ProductShipped(Sku, quantity, DateTime.UtcNow);
Apply(productShipped);
Add(productShipped);
}
private void Apply(ProductShipped evnt)
{
_warehouseProductState.QuantityOnHand -= evnt.Quantity;
}
public void ReceiveProduct(int quantity)
{
var productReceived = new ProductReceived(Sku, quantity, DateTime.UtcNow);
Apply(productReceived);
Add(productReceived);
}
private void Apply(ProductReceived evnt)
{
_warehouseProductState.QuantityOnHand += evnt.Quantity;
}
public void AdjustInventory(int quantity, string reason)
{
if (_warehouseProductState.QuantityOnHand + quantity < 0)
{
throw new InvalidDomainException("Cannot adjust to a negative Quantity on Hand.");
}
var inventoryAdjusted = new InventoryAdjusted(Sku, quantity, reason, DateTime.UtcNow);
Apply(inventoryAdjusted);
Add(inventoryAdjusted);
}
private void Apply(InventoryAdjusted evnt)
{
_warehouseProductState.QuantityOnHand += evnt.Quantity;
}
public WarehouseProductState GetState()
{
return _warehouseProductState;
}
public int GetQuantityOnHand()
{
return _warehouseProductState.QuantityOnHand;
}
}
public class InvalidDomainException : Exception
{
public InvalidDomainException(string message) : base(message)
{
}
}
}

The WarehouseProduct above has 3 commands: ShipProduct, ReceiveProduct, and AdjustInventory. All of which result in creating their respective events if they passed any invariants.

To illustrate this Given, When, Then for the ShipProduct command, which should create a ProductShipped Event.

public class WarehouseProductTests
{
private readonly string _sku;
private readonly int _initialQuantity;
private readonly WarehouseProduct _sut;
private readonly Fixture _fixture;
public WarehouseProductTests()
{
_fixture = new Fixture();
_fixture.Customizations.Add(new Int32SequenceGenerator());
_sku = _fixture.Create<string>();
_initialQuantity = (int)_fixture.Create<uint>();
_sut = WarehouseProduct.Load(_sku, new [] {
new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow)
});
}
[Fact]
public void ShipProductShouldRaiseProductShipped()
{
var quantityToShip = _fixture.Create<int>();
_sut.ShipProduct(quantityToShip);
var outEvents = _sut.GetUncommittedEvents();
outEvents.Count.ShouldBe(1);
var outEvent = outEvents.Single();
outEvent.ShouldBeOfType<ProductShipped>();
var productShipped = (ProductShipped)outEvent;
productShipped.ShouldSatisfyAllConditions(
x => x.Quantity.ShouldBe(quantityToShip),
x => x.Sku.ShouldBe(_sku),
x => x.EventType.ShouldBe("ProductShipped")
);
}
}
view raw Test.cs hosted with ❤ by GitHub

While that does satisfy our goal, it’s a bit cumbersome to have to write this to verify the event. To simply and make this a bit more natural to read, I created a base class to use within our tests.

using System;
using System.Linq;
using Shouldly;
namespace EventSourcing.Demo
{
public abstract class AggregateTests<TAggregate> where TAggregate : AggregateRoot
{
private readonly TAggregate _aggregateRoot;
protected AggregateTests(TAggregate aggregateRoot)
{
_aggregateRoot = aggregateRoot;
}
protected void Given(params IEvent[] events)
{
if (events != null)
{
_aggregateRoot.Load(events);
}
}
protected void When(Action<TAggregate> command)
{
command(_aggregateRoot);
}
protected void Then<TEvent>(params Action<TEvent>[] conditions)
{
var events = _aggregateRoot.GetUncommittedEvents();
events.Count.ShouldBe(1);
var evnt = events.First();
evnt.ShouldBeOfType<TEvent>();
if (conditions != null)
{
((TEvent)evnt).ShouldSatisfyAllConditions(conditions);
}
}
protected void Throws<TException>(Action<TAggregate> command, params Action<TException>[] conditions) where TException : Exception
{
var ex = Should.Throw<TException>(() => command(_aggregateRoot));
if (conditions != null)
{
ex.ShouldSatisfyAllConditions(conditions);
}
}
}
}

Now using this abstract class, here are all the tests for the WarehouseProduct.

using System;
using AutoFixture;
using Shouldly;
using Xunit;
namespace EventSourcing.Demo
{
public class WarehouseProductAggregateRootTests : AggregateTests<WarehouseProduct>
{
private readonly Fixture _fixture;
private readonly string _sku = "abc123";
private readonly int _initialQuantity;
public WarehouseProductAggregateRootTests() : base(new WarehouseProduct("abc123"))
{
_fixture = new Fixture();
_fixture.Customizations.Add(new Int32SequenceGenerator());
_initialQuantity = (int)_fixture.Create<uint>();
}
[Fact]
public void ShipProductShouldRaiseProductShipped()
{
Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow));
var quantityToShip = _fixture.Create<int>();
When(x => x.ShipProduct(quantityToShip));
Then<ProductShipped>(
x => x.Quantity.ShouldBe(quantityToShip),
x => x.Sku.ShouldBe(_sku),
x => x.EventType.ShouldBe("ProductShipped"));
}
[Fact]
public void ShipProductShouldThrowIfNoQuantityOnHand()
{
Given();
Throws<InvalidDomainException>(
x => x.ShipProduct(1),
x => x.Message.ShouldBe("Cannot Ship to a negative Quantity on Hand."));
}
[Fact]
public void ReceiveProductShouldRaiseProductReceived()
{
Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow));
var quantityToReceive = _fixture.Create<int>();
When(x => x.ReceiveProduct(quantityToReceive));
Then<ProductReceived>(
x => x.Quantity.ShouldBe(quantityToReceive),
x => x.Sku.ShouldBe(_sku),
x => x.EventType.ShouldBe("ProductReceived"));
}
[Fact]
public void AdjustInventoryShouldRaiseProductAdjusted()
{
Given(new ProductReceived(_sku, _initialQuantity, DateTime.UtcNow));
var quantityToAdjust = _fixture.Create<int>();
var reason = _fixture.Create<string>();
When(x => x.AdjustInventory(quantityToAdjust, reason));
Then<InventoryAdjusted>(
x => x.Quantity.ShouldBe(quantityToAdjust),
x => x.Sku.ShouldBe(_sku),
x => x.Reason.ShouldBe(reason),
x => x.EventType.ShouldBe("InventoryAdjusted"));
}
[Fact]
public void AdjustInventoryShouldThrowIfNoQuantityOnHand()
{
Given();
var reason = _fixture.Create<string>();
Throws<InvalidDomainException>(
x => x.AdjustInventory(-1, reason),
x => x.Message.ShouldBe("Cannot adjust to a negative Quantity on Hand."));
}
}
}

Source Code

Developer-level members of my CodeOpinion YouTube channel get access to the full source for any working demo application that I post on my blog or YouTube. Check out the membership for more info.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


Leave a Reply

Your email address will not be published. Required fields are marked *