Skip to content

Testing with Cratis

Testing

Turn the model into executable examples

Cratis applications already describe behavior as a timeline: a screen sends a command, the command records an event, projections build read models, and reactors act on facts. The testing story follows the same shape. You write a small specification for the behavior, seed the facts that already happened, perform the command or event, and assert the new facts, read models, or side effects.

The packages are layered the same way the runtime is layered:

PackageUse it for
Cratis.Specifications.XUnitBDD-style specifications on top of xUnit: Establish() for given, Because() for when, [Fact] methods for then, plus Should* assertions and Catch.Exception.
Cratis.Specifications.NUnitThe same specification style for NUnit projects.
Cratis.Arc.TestingCommandScenario<TCommand> for running Arc commands through the real command pipeline without HTTP.
Cratis.Chronicle.TestingEventScenario, ReadModelScenario<TReadModel>, and ReactorScenario<TReactor> for in-process Chronicle tests without a server or database.
Cratis.Arc.Chronicle.TestingAutomatically extends CommandScenario<TCommand> with an in-memory Chronicle event log when the package is referenced.
Cratis.TestingConvenience package for Arc + Chronicle application tests. Use it with Cratis.Specifications.XUnit.

For most Cratis stack slices, reference:

<PackageReference Include="Cratis.Specifications.XUnit" />
<PackageReference Include="Cratis.Testing" />

Use Cratis.Chronicle.Testing directly for Chronicle-only libraries, and Cratis.Arc.Testing directly when a slice deliberately has no event-sourced write side.

The given/when/then shape is not an arbitrary testing style here. It is the same grammar as event sourcing:

Specification wordEvent-sourced meaningCratis test API
GivenFacts that already happened, or read model state that already existsEventScenario.Given, ReadModelScenario.Given, reusable given base contexts
WhenThe command, append, projection input, or reactor trigger under testCommandScenario.Execute, EventLog.Append, ReactorScenario.Given.ForEventSource(...).Events(...)
ThenThe observable result: appended events, command result, projected read model, or side effect[Fact] methods with ShouldBeSuccessful, ShouldHaveAppendedEvent, ShouldEqual, or mock assertions

That makes specs readable in the same language as the event model. A domain expert can read “given an author already exists, when registering the same name, then it should not append another event” and understand the rule without knowing the implementation.

Start with one column from an event model:

UI/A: AuthorsUI/A: AuthorsC/RM: AuthorsC/RM: AuthorsStream: Authors
RegisterAuthorScreen
RegisterAuthor



authorId: uuid, name: string
AuthorRegistered



name: string
Author
AuthorsScreen

Read it as a testable behavior:

GivenWhenThen
no author exists for this idRegisterAuthor is executedthe command succeeds
no author exists for this idRegisterAuthor is executedAuthorRegistered is appended for the author id
AuthorRegistered existsthe projection handles itthe Author read model contains the name the screen needs

The slice can stay small:

using Cratis.Arc.Commands;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections;
using Cratis.Chronicle.ReadModels;
[Command]
public record RegisterAuthor([property: Key] EventSourceId AuthorId, string Name)
{
public AuthorRegistered Handle() => new(Name);
}
[EventType]
public record AuthorRegistered(string Name);
[ReadModel]
[FromEvent<AuthorRegistered>]
public record Author([property: Key] EventSourceId Id, string Name);

And the stack spec follows the same order:

using Cratis.Arc.Chronicle.Testing.Commands;
using Cratis.Arc.Commands;
using Cratis.Arc.Testing.Commands;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing.ReadModels;
using Cratis.Specifications;
using Xunit;
public class when_registering_a_new_author : Specification
{
readonly EventSourceId _authorId = EventSourceId.New();
readonly CommandScenario<RegisterAuthor> _command = new();
readonly ReadModelScenario<Author> _projection = new();
CommandResult _result = default!;
async Task Because()
{
_result = await _command.Execute(new RegisterAuthor(_authorId, "Jane Austen"));
await _projection.Given
.ForEventSource(_authorId)
.Events(_command.AppendedEvents.Select(_ => _.Event.Content).ToArray());
}
[Fact] void should_accept_the_command() =>
_result.ShouldBeSuccessful();
[Fact] Task should_record_the_fact() =>
_command.ShouldHaveAppendedEvent<RegisterAuthor, AuthorRegistered>(
_authorId,
e => e.Name == "Jane Austen");
[Fact] void should_project_the_author_for_the_screen() =>
_projection.Instance!.Name.ShouldEqual("Jane Austen");
}

There is no HTTP server in that spec and no Chronicle server to start. Arc runs the command through the real command pipeline. The Chronicle extension captures the events appended by the command. The read model scenario then feeds those event contents through the projection in-process, so the final assertion checks the same state the React query would read.

Use Arc tests for command behavior: validation, authorization, injected services, and whether the command succeeds. CommandScenario<TCommand> builds the real Arc command pipeline lazily, so you register services before Because() calls Execute() or Validate().

public class when_adding_item_to_cart : Specification
{
readonly IInventoryService _inventory = Substitute.For<IInventoryService>();
readonly CommandScenario<AddItemToCart> _scenario = new();
CommandResult _result = default!;
void Establish()
{
_inventory.IsInStock("SKU-123").Returns(true);
_scenario.Services.AddSingleton(_inventory);
}
async Task Because() =>
_result = await _scenario.Execute(new AddItemToCart("SKU-123", 2));
[Fact] void should_succeed() =>
_result.ShouldBeSuccessful();
}

Use Validate() when you want validation and authorization without running the handler. Use the Chronicle extension when the command returns events and you want to assert which facts were recorded.

Chronicle tests are fastest when you test the part of the event pipeline you own:

You need to proveUse
Code appends the right event, or rejects an append because of constraintsEventScenario
A projection or reducer builds the read model correctlyReadModelScenario<TReadModel>
A reactor performs the right side effect when an event arrivesReactorScenario<TReactor>

For example, a projection spec is just given events, then read model state:

public class when_projecting_a_registered_author : Specification
{
readonly EventSourceId _authorId = EventSourceId.New();
readonly ReadModelScenario<Author> _scenario = new();
Task Because() =>
_scenario.Given
.ForEventSource(_authorId)
.Events(new AuthorRegistered("Jane Austen"));
[Fact] void should_set_the_author_name() =>
_scenario.Instance!.Name.ShouldEqual("Jane Austen");
}

Use a real Chronicle integration test when you are testing hosting, storage, subscriptions, observer recovery, or the boundary between processes. Keep the bulk of slice behavior in the in-process scenarios so the suite stays fast.

A full slice usually has three useful spec levels:

LevelWhat it provesTypical tool
Command specThe command validates, authorizes, and records the right factsCommandScenario<TCommand> + Cratis.Arc.Chronicle.Testing
Read side specThe event history builds the read model the screen needsReadModelScenario<TReadModel>
Workflow specThe app boundary, identity, tenant routing, real Chronicle host, and projections work togetherxUnit fixture + Cratis.Specifications.XUnit + HTTP/client helpers

Start with command and read-side specs for every meaningful event-model column. Add workflow specs around the edges where the wiring matters: AuthProxy headers, tenant isolation, a real Chronicle server, or browser-visible behavior.