Skip to main content

Testing (make yourself dangerous)

Event sourcing is unusually testable because:

  • your β€œdatabase writes” are events (data)
  • your business logic is in aggregate methods (pure-ish)

You want tests that read like:

Given history, when command, then new events (or error)

Minimal test harness​

public static class EsTest
{
public static BankAccount Given(params IDomainEvent[] history)
{
var acc = new BankAccount();
acc.LoadFromHistory(history);
return acc;
}

public static IReadOnlyList<IDomainEvent> When(BankAccount acc, Action<BankAccount> act)
{
act(acc);
return acc.DequeueUncommittedEvents();
}
}

Example tests (xUnit-style)​

using Xunit;

public sealed class BankAccountTests
{
[Fact]
public void Withdraw_rejects_if_insufficient_funds()
{
var acc = EsTest.Given(
new AccountOpened(Guid.Parse("11111111-1111-1111-1111-111111111111"), "Jeevan"),
new MoneyDeposited(10m)
);

var ex = Assert.Throws<InvalidOperationException>(() => acc.Withdraw(50m));
Assert.Equal("Insufficient funds.", ex.Message);
}

[Fact]
public void Withdraw_emits_event_when_valid()
{
var acc = EsTest.Given(
new AccountOpened(Guid.Parse("11111111-1111-1111-1111-111111111111"), "Jeevan"),
new MoneyDeposited(100m)
);

var emitted = EsTest.When(acc, a => a.Withdraw(30m));

Assert.Collection(emitted,
e => Assert.Equal(new MoneyWithdrawn(30m), e)
);
}

[Fact]
public void Close_prevents_further_deposits()
{
var acc = EsTest.Given(
new AccountOpened(Guid.Parse("11111111-1111-1111-1111-111111111111"), "Jeevan"),
new AccountClosed(DateTimeOffset.Parse("2025-01-01T00:00:00Z"))
);

var ex = Assert.Throws<InvalidOperationException>(() => acc.Deposit(10m));
Assert.Equal("Account is closed.", ex.Message);
}
}

Property-based tests (optional, high leverage)​

If you want to level up fast:

  • generate random sequences of deposits/withdrawals
  • assert invariants always hold (balance never < 0, closed account never changes, etc.)

You can do this with FsCheck, but the core idea matters more than the tool.

Next​

Now we tackle the hard part: running this in production over time.

Next: Operations + evolution