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.

Each backend tests a command through the real Arc command pipeline and a read model without a Chronicle server. The packages differ:

BackendCommand specsRead-model specs
C#Cratis.Arc.Testing (CommandScenario<TCommand>), with Cratis.Arc.Chronicle.Testing for the events a command appendsCratis.Chronicle.Testing (ReadModelScenario<TReadModel>)
Kotlinio.cratis:arc-testing (CommandScenario), with io.cratis:arc-chronicle-spring-boot-starter for the events a command appendsio.cratis:chronicle-testing (ReadModelScenario)
Javaio.cratis:arc-testing, driven through BlockingCommandScenario, with the same Chronicle starterThe examples call the reducer directly
TypeScript (preview)@cratis/arc.testing (CommandScenario), with @cratis/arc.chronicle/testing (ChronicleCommandScenario) for the events a command appends@cratis/chronicle/testing (ReadModelScenario)

The Arc for Kotlin and Java testing guide and the Arc for TypeScript testing guide cover the JVM and TypeScript harnesses in depth. The rest of this section lists the .NET packages.

For .NET, 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 .NET 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.

Given/when/then maps one to one onto event sourcing: the given is the facts that already happened, the when is the command or event under test, and the then is the facts, read models, or side effects that result. The .NET specification APIs line up like this:

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 command’s handler returns the event, and Arc appends it to the event source the command identifies. The slice stays small: each snippet holds only the RegisterAuthor command and the AuthorRegistered event. The Author read model from the event model comes in with the spec under Spec the read side.

using Cratis.Arc.Commands.ModelBound;
using Cratis.Chronicle.Events;
namespace Library.Authors;
[Command]
public record RegisterAuthor(EventSourceId AuthorId, string Name)
{
public AuthorRegistered Handle() => new(Name);
}
[EventType]
public record AuthorRegistered(string Name);

View C# snippet source on GitHub

The command spec covers the first two rows: the command succeeds, and AuthorRegistered is recorded for the author id.

using Cratis.Arc.Chronicle.Testing.Commands;
using Cratis.Arc.Commands;
using Cratis.Arc.Testing.Commands;
using Cratis.Chronicle.Events;
using Cratis.Specifications;
using Xunit;
namespace Library.Authors.for_RegisterAuthor;
public class when_registering_a_new_author : Specification
{
readonly EventSourceId _authorId = EventSourceId.New();
readonly CommandScenario<RegisterAuthor> _scenario = new();
CommandResult _result = default!;
async Task Because() => _result = await _scenario.Execute(new RegisterAuthor(_authorId, "Jane Austen"));
[Fact] void should_accept_the_command() => _result.ShouldBeSuccessful();
[Fact] Task should_record_the_fact() =>
_scenario.ShouldHaveAppendedEvent<RegisterAuthor, AuthorRegistered>(
_authorId,
e => e.Name == "Jane Austen");
void Destroy() => _scenario.Dispose();
}

View C# snippet source on GitHub

There is no HTTP server in that spec and no Chronicle server to start. Arc runs the command through the real command pipeline, and each backend’s Chronicle test support captures the events the command appends in memory. In Kotlin and Java, shouldHaveAppendedEvent expects exactly one matching event; C# (ShouldHaveAppendedEvent) and TypeScript accept one or more. TypeScript’s ChronicleCommandScenario does not enforce constraints; the Arc for TypeScript testing guide lists what it does not cover.

The read-side spec covers the last row: given AuthorRegistered, the Author read model contains the name the screen needs. C#, Kotlin, and TypeScript run the read model in process with a read-model scenario; Java calls its reducer directly. Elixir does not support this test yet. Each example declares AuthorRegistered again so it compiles on its own (in your application, reuse the event from the slice) and introduces the Author read model. These tabs are the Chronicle client set, which also includes Elixir. They are separate from the Arc backend tabs above, so picking a backend there does not switch them.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
using Cratis.Chronicle.Testing.ReadModels;
using Cratis.Specifications;
using Xunit;
[EventType]
public record AuthorRegistered(string Name);
[FromEvent<AuthorRegistered>]
public record Author([Key] Guid Id, string Name);
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");
}

View C# snippet source on GitHub

In C#, one spec can cover both sides: pass the contents of the command scenario’s AppendedEvents to ReadModelScenario<TReadModel>.Given.ForEventSource(...).Events(...), and the last assertion checks the same state the React query reads.

Use Arc tests for command behavior: validation, authorization, injected services, and whether the command succeeds. These examples register a service before executing a command that calls it through the Arc pipeline:

using System.Threading.Tasks;
using Cratis.Arc.Commands;
using Cratis.Arc.Testing.Commands;
using Cratis.Specifications;
using Library.Authors;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
namespace Library.Specs;
public class when_recording_an_author : Specification
{
readonly CommandScenario<RecordAuthor> _scenario = new();
readonly AuthorId _id = AuthorId.New();
readonly AuthorName _name = new("Ada Lovelace");
IAuthorRegistration _registration = null!;
CommandResult _result = null!;
void Establish()
{
_registration = Substitute.For<IAuthorRegistration>();
_registration.Register(_id, _name).Returns(Task.CompletedTask);
_scenario.Services.AddSingleton(_registration);
}
async Task Because() => _result = await _scenario.Execute(new RecordAuthor(_id, _name));
[Fact] void should_succeed() => _result.ShouldBeSuccessful();
[Fact] async Task should_register_the_author() => await _registration.Received(1).Register(_id, _name);
void Destroy() => _scenario.Dispose();
}

View C# snippet source on GitHub

In .NET, 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.

In .NET, 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>

Use a real Chronicle integration test when you are testing hosting, storage, subscriptions, observer recovery, or the boundary between processes. In .NET, Kotlin, and TypeScript, keep supported read-model behavior in the in-process scenarios so the suite stays fast.

A full .NET 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.