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.