Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Eventuous/eventuous/llms.txt

Use this file to discover all available pages before exploring further.

The Eventuous.Testing package provides AggregateSpec<TAggregate, TState> — a base class that lets you unit-test your aggregate domain logic in complete isolation, with no event store or infrastructure required. You express each test scenario in the familiar Given/When/Then pattern: pre-load prior events with GivenEvents, call your domain method in When, then verify the resulting emitted events with Emitted or inspect aggregate state through Then.

NuGet package

<PackageReference Include="Eventuous.Testing" Version="*" />

AggregateSpec<TAggregate, TState>

public abstract class AggregateSpec<TAggregate, TState>(AggregateFactoryRegistry? registry = null)
    where TAggregate : Aggregate<TState>
    where TState     : State<TState>, new()
Inherit from this class to create a test suite for a specific aggregate. Each subclass represents one scenario (the When), and individual test methods make one or more assertions against the aggregate produced by that scenario.

Constructor parameter

ParameterDescription
registryOptional AggregateFactoryRegistry. When omitted, AggregateFactoryRegistry.Instance (the global registry) is used.

GivenEvents() — prior event history

protected virtual object[] GivenEvents() => [];
Override this to return the domain events that should be loaded into the aggregate before When is called. The default implementation returns an empty array, representing a brand-new aggregate. Events are loaded via Aggregate.Load, so every On<TEvent> handler in your State class fires exactly as it would in production.

When(TAggregate aggregate) — the action under test

protected abstract void When(TAggregate aggregate);
Implement this method to call the single domain operation your scenario exercises. The aggregate instance has already been hydrated from GivenEvents() when this method is called.

CreateInstance() — aggregate factory

protected virtual TAggregate CreateInstance()
    => Registry.CreateInstance<TAggregate, TState>();
Called internally by Then() to construct a fresh aggregate instance. Override this when your aggregate requires constructor arguments that the factory registry cannot supply automatically.

Then() — execute and return the aggregate

protected TAggregate Then()
Instantiates the aggregate via CreateInstance(), loads GivenEvents(), and calls When(). Returns the resulting aggregate so you can inspect State, Changes, or any other aggregate property directly in a test method. Calling Then() multiple times is safe — each call rebuilds the aggregate from scratch.

Emitted(params object[] events) — assert emitted events

protected TAggregate Emitted(params object[] events)
Asserts that every event in events is a member of aggregate.Changes (the events raised during When). Uses subset semantics — the aggregate may emit additional events beyond those you assert. Calls Then() internally if it has not been called yet, so a minimal test method can consist of a single Emitted(...) call. Returns the aggregate for method chaining or further inspection.

Instance — last produced aggregate

protected TAggregate? Instance { get; private set; }
Holds the aggregate produced by the most recent Then() call. Use this when you need to inspect state across multiple test methods without triggering When() again.

AggregateWithIdSpec<TAggregate, TState, TId>

public abstract class AggregateWithIdSpec<TAggregate, TState, TId>(AggregateFactoryRegistry? registry = null)
    : AggregateSpec<TAggregate, TState>(registry)
    where TAggregate : Aggregate<TState>
    where TState     : State<TState, TId>, new()
    where TId        : Id
A variant of AggregateSpec for aggregates whose State type carries a typed identity (State<TState, TId>). Automatically seeds the aggregate identity before When is called, so the state’s Id property is populated exactly as it would be when loading from a real event store.

Id — the aggregate identity

protected abstract TId? Id { get; }
Return the identity value that should be assigned to the aggregate under test. When Id is null, the base CreateInstance() behaviour applies and no identity is pre-assigned. AggregateWithIdSpec overrides CreateInstance() to call Registry.CreateTestAggregateInstance<TAggregate, TState, TId>(Id) when Id is non-null.

AggregateFactoryRegistry

The registry controls how aggregate instances are constructed. The global singleton is AggregateFactoryRegistry.Instance. Pass a custom registry to the spec constructor only when you have non-standard construction requirements (e.g. aggregate constructors that accept injected services). For the vast majority of aggregates the default behaviour — constructing via the parameterless factory — is correct and requires no configuration.

Complete example

The following example tests a Booking aggregate. The Given scenario starts from a room-booked event, the When records a payment, and several test methods each assert a different aspect of the outcome.
using Eventuous.Testing;
using NodaTime;

namespace Bookings.Tests;

// ──── Domain types (abbreviated for clarity) ────────────────────────────────

[EventType("V1.RoomBooked")]
public record RoomBooked(
    string    RoomId,
    LocalDate CheckIn,
    LocalDate CheckOut,
    float     BookingPrice);

[EventType("V1.PaymentRecorded")]
public record PaymentRecorded(float PaidAmount, float Outstanding, string PaymentId);

[EventType("V1.BookingFullyPaid")]
public record BookingFullyPaid(DateTimeOffset FullyPaidAt);

public record BookingState : State<BookingState>
{
    public float Outstanding { get; init; }
    public bool  Paid        { get; init; }

    public BookingState()
    {
        On<RoomBooked>((state, e) => state with { Outstanding = e.BookingPrice });
        On<PaymentRecorded>((state, e) => state with { Outstanding = e.Outstanding });
        On<BookingFullyPaid>((state, _) => state with { Paid = true });
    }
}

public class Booking : Aggregate<BookingState>
{
    public void RecordPayment(float amount, string paymentId, DateTimeOffset paidAt)
    {
        EnsureExists();
        var outstanding = State.Outstanding - amount;
        Apply(new PaymentRecorded(amount, outstanding, paymentId));
        if (outstanding <= 0) Apply(new BookingFullyPaid(paidAt));
    }
}

// ──── Test spec ──────────────────────────────────────────────────────────────

public class BookingPaymentSpec : AggregateSpec<Booking, BookingState>
{
    static readonly DateTimeOffset PaidAt = DateTimeOffset.UtcNow;

    // Given: a room was already booked at €100
    protected override object[] GivenEvents() =>
    [
        new RoomBooked("room-42", LocalDate.FromDateTime(DateTime.Today),
                                  LocalDate.FromDateTime(DateTime.Today.AddDays(3)),
                                  100f)
    ];

    // When: the full amount is recorded as a payment
    protected override void When(Booking booking)
        => booking.RecordPayment(100f, "pay-001", PaidAt);

    [Test]
    public void should_emit_payment_recorded()
        => Emitted(new PaymentRecorded(100f, 0f, "pay-001"));

    [Test]
    public void should_emit_fully_paid()
        => Emitted(new BookingFullyPaid(PaidAt));

    [Test]
    public async Task should_mark_booking_as_paid()
        => await Assert.That(Then().State.Paid).IsTrue();

    [Test]
    public async Task should_produce_two_events()
        => await Assert.That(Then().Changes).HasCount(2);
}

Using AggregateWithIdSpec

When your aggregate state type carries a typed Id property, use the AggregateWithIdSpec variant to have the identity automatically wired up:
public record BookingId(string Value) : Id(Value);

public record BookingStateWithId : State<BookingStateWithId, BookingId>;

public class BookingWithId : Aggregate<BookingStateWithId>
{
    public void Process() => Apply(new BookingInitiated());
}

[EventType("BookingInitiated")]
public record BookingInitiated;

public class BookingWithIdSpec : AggregateWithIdSpec<BookingWithId, BookingStateWithId, BookingId>
{
    protected override BookingId? Id { get; } = new("booking-99");

    protected override void When(BookingWithId aggregate)
        => aggregate.Process();

    [Test]
    public void should_emit_initiated_event()
        => Emitted(new BookingInitiated());

    [Test]
    public async Task should_carry_the_correct_id()
        => await Assert.That(Then().State.Id.Value).IsEqualTo("booking-99");
}

Testing patterns

One spec class per scenario — keep each AggregateSpec subclass focused on a single When action. Multiple test methods in the class then cover different assertions about that one outcome, and you can freely call both Emitted(...) and Then() within the same spec without repeating setup code. Combine Emitted and Then in the same specEmitted performs event subset assertions and returns the aggregate, while Then gives you access to State, Changes, and any public aggregate properties for richer behavioral assertions. Don’t assert order unless it mattersEmitted uses subset semantics. If event order is significant for your domain, inspect Then().Changes directly with your test framework’s ordering assertions. No IEventStore requiredAggregateSpec never touches a database or event store. The aggregate is hydrated and exercised entirely in memory, keeping tests fast and free of infrastructure setup.

Build docs developers (and LLMs) love