Testing Event-Sourced Applications
If you use Dispatch without event sourcing (e.g., as a MediatR replacement), the Testing Dispatch Handlers guide covers unit testing handlers, verifying middleware behavior, and integration testing the pipeline.
Testing event-sourced applications requires different strategies than traditional CRUD applications. Instead of asserting database state, you verify that the correct events are raised and that aggregates behave correctly when replaying historical events.
Before You Start
- .NET 8.0+ (or .NET 9/10 for latest features)
- Install the testing packages:
dotnet add package Excalibur.Dispatch.Testing
dotnet add package Excalibur.Dispatch.Testing.Shouldly # optional - Familiarity with handlers and event sourcing concepts
Testing Pyramid for Event Sourcing
┌─────────────────┐
│ E2E Tests │ ← Full system with real transport
│ (few) │
├─────────────────┤
│ Integration │ ← Real database, real event store
│ (some) │
├─────────────────┤
│ Unit Tests │ ← In-memory, fast, isolated
│ (many) │
└─────────────────┘
What to Test
| Layer | What to Test | How |
|---|---|---|
| Aggregates | Business rules, state transitions, event generation | AggregateTestFixture |
| Event Handlers | Projections update correctly, side effects triggered | Mock event store |
| Repositories | Load/save roundtrips, concurrency handling | Integration tests with real store |
| Sagas | State machine transitions, compensating actions | AggregateTestFixture pattern |
Excalibur.Testing Package
Install the testing utilities:
dotnet add package Excalibur.Testing
This package provides:
AggregateTestFixture<T>— Fluent Given-When-Then API for aggregate testing- Conformance Test Kits — Verify custom provider implementations against contracts
Test Framework Compatibility
Excalibur.Testing is framework-agnostic. Use it with:
- xUnit (recommended)
- NUnit
- MSTest
- Any other .NET test framework
The fixture throws TestFixtureAssertionException on failures, which all test runners recognize as test failures.
Quick Example
using Excalibur.Testing;
using Xunit;
public class OrderTests
{
[Fact]
public void Create_order_raises_OrderCreated_event()
{
new AggregateTestFixture<Order>()
.Given() // No prior events - new aggregate
.When(order => order.Create("customer-123"))
.Then()
.ShouldRaise<OrderCreated>(e => e.CustomerId == "customer-123");
}
[Fact]
public void Cannot_ship_cancelled_order()
{
new AggregateTestFixture<Order>()
.Given(
new OrderCreated { OrderId = "123", CustomerId = "C1" },
new OrderCancelled { OrderId = "123", Reason = "Out of stock" })
.When(order => order.Ship())
.ShouldThrow<InvalidOperationException>("cancelled");
}
}
Testing Categories
Aggregate Testing
Unit test your aggregates using the Given-When-Then pattern. Fast, isolated, no external dependencies.
Repository Testing
Integration test your event store interactions. Verify load/save roundtrips and concurrency.
Integration Tests
End-to-end testing with real infrastructure using TestContainers.
Dispatch Pipeline Testing
The Excalibur.Dispatch.Testing package provides purpose-built test infrastructure for the Dispatch pipeline. These tools let you test handlers, middleware, and transport interactions without real message brokers.
dotnet add package Excalibur.Dispatch.Testing
dotnet add package Excalibur.Dispatch.Testing.Shouldly # optional, for fluent assertions
Test Harness
DispatchTestHarness builds a real Dispatch pipeline with DI, automatically tracks all dispatched messages, and provides MessageContextBuilder for creating test contexts with sensible defaults.
Transport Test Doubles
In-memory implementations of ITransportSender, ITransportReceiver, and ITransportSubscriber that record all interactions for assertions. Test transport flows without RabbitMQ, Kafka, or any real infrastructure.
Shouldly Assertions
Nine fluent assertion extensions for IDispatchedMessageLog, InMemoryTransportSender, and InMemoryTransportReceiver. Produces domain-specific failure messages.
Testing Dispatch Handlers
Unit test handlers directly with FakeItEasy, test middleware behavior, and integration test the full pipeline.
Best Practices
DO
- Test one behavior per test method
- Use descriptive test names that explain the scenario
- Test both happy paths and error cases
- Verify events are raised with correct data
- Test idempotency where applicable
DON'T
- Test implementation details (private methods)
- Skip negative test cases
- Couple tests to event serialization format
- Test the framework itself (trust Excalibur's tests)
Next Steps
- Start with Aggregate Testing for unit tests
- Add Repository Testing for integration coverage
- See Integration Tests for full system tests
- Use the Test Harness for pipeline-level testing
See Also
- Testing Handlers - Unit testing handler implementations
- Test Harness - DispatchTestHarness for pipeline-level testing
- Integration Tests - Full system integration testing