Skip to content

Testing

Chronicle provides in-process scenarios for testing event appends and read models without a running kernel. The .NET and TypeScript testing libraries also cover reactor side effects, and .NET offers integration fixtures. Cratis.Chronicle.XUnit.Integration exercises the full client and kernel stack against MongoDB via Testcontainers when you need to prove storage, network, or full-stack behavior.

In .NET applications, Chronicle tests usually sit inside Cratis Specifications. That makes the event-sourced shape explicit: Establish() or a scenario’s Given builder seeds facts that already happened, Because() appends or provides the event under test, and each [Fact] asserts the new facts, read model state, or side effect.

Start with the in-process scenarios your client supports. They test the slice without infrastructure; use a kernel-backed test when you need to prove behavior the client’s scenario does not cover. The table below names the .NET scenarios and integration fixtures:

What to proveStart withWhat it does not prove
Event appends, constraint violations, sequencingEventScenarioNetwork serialization, kernel connection, storage write
Read model projection or reducer logicReadModelScenario<T>Sink writes, MongoDB queries, change stream subscription
Reactor side effects and event orderingReactorScenario<T>Network observer registration, kernel event dispatch, redelivery — each event is handed to the reactor once, so a repeated delivery (and the idempotence it requires) needs a spec that delivers the event twice
Actual storage writes, kernel connectivity, change streamsChronicleInProcessFixture / ChronicleOutOfProcessFixtureClient isolation—the kernel is shared across tests

The .NET integration fixtures available in Cratis.Chronicle.XUnit.Integration are:

  • ChronicleInProcessFixture — an in-process Chronicle kernel with MongoDB via Testcontainers.
  • ChronicleOutOfProcessFixture — a Chronicle kernel in a separate container, also using Testcontainers.
  • ChronicleClientFixture — a base for integration tests that only need a Chronicle client reference.
  • ChronicleOrleansFixture — an Orleans-based Chronicle kernel for testing Chronicle’s own kernel internals.

Only the in-process and out-of-process fixtures are documented here; the others are for framework or advanced scenarios.

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

Cratis.Chronicle.Testing brings in Cratis.Arc and Cratis.Arc.Core, including the ASP.NET Core shared framework. You do not need to reference Arc separately for Chronicle-only tests.

Chronicle and Arc have independent release numbers. Match Cratis.Chronicle.Testing to your Chronicle client release; its NuGet dependency list identifies the Arc version that release targets. For example, in the Chronicle.Testing 19.26.x version line, 19.26.0 targets Arc 22.30.0. Check each release’s dependency list rather than assuming all releases in that line target the same Arc version. A Cratis meta-package release can target an earlier Chronicle client, so its version alone does not identify a matching Chronicle.Testing package.

To use a newer compatible Arc 22.x release with an older Chronicle.Testing release, pin both Cratis.Arc and Cratis.Arc.Core to the same version in Directory.Packages.props and reference them directly in the test project. Alternatively, enable CentralPackageTransitivePinningEnabled in that file to apply the central pins to transitive dependencies. Align any other Arc runtime packages your application uses with that version. Do not downgrade below Chronicle.Testing’s declared dependencies; verify your scenarios after overriding them.

Use Cratis.Testing instead of Cratis.Chronicle.Testing when the same spec also tests Arc commands.

Application command execution scopes, including Chronicle transactions, belong to the outer Arc command pipeline. The in-process scenario helpers do not discover or invoke your application’s ICommandExecutionScope implementations when processing an event append. Your application scopes still run around the application command; you do not need to register their dependencies with the scenario harness.

The .NET scenarios accept per-run Defaults with ChronicleOptions.ExplicitArtifacts. The same nominated event types, reducers, reactors, constraints and migrations feed actual scenario processing. A plain ReactorScenario<T> requires explicit reactor nomination; it does not discover arbitrary classes by shape.

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing;
using Cratis.Chronicle.Testing.EventSequences;
using Cratis.Chronicle.Testing.ReadModels;
using Cratis.Chronicle.Testing.Reactors;
using Cratis.Specifications;
namespace ExplicitScenarioExample
{
public record Credited(int Amount);
public record CreditNoted(int Amount);
public record Balance(int Amount);
public class BalanceReducer
{
public Balance Reduce(Credited @event, Balance? current) =>
new((current?.Amount ?? 0) + @event.Amount);
}
public class Notifier
{
public CreditNoted On(Credited @event) => new(@event.Amount);
}
public static class Scenarios
{
public static async Task Verify()
{
var options = new ChronicleOptions();
options.ExplicitArtifacts.RegisterEventType<Credited>().RegisterEventType<CreditNoted>()
.RegisterReducer<BalanceReducer>().RegisterReactor<Notifier>();
var defaults = new Defaults(new InMemoryClientArtifactsProvider(), null, options);
var source = EventSourceId.New();
var readModel = new ReadModelScenario<Balance>(null, defaults);
await readModel.Given.ForEventSource(source).Events(new Credited(3), new Credited(4));
readModel.Instance!.Amount.ShouldEqual(7);
using var reactor = new ReactorScenario<Notifier>(defaults);
await reactor.Given.ForEventSource(source).Events(new Credited(3));
reactor.ShouldHaveProduced<CreditNoted>(_ => _.Amount == 3);
using var events = new EventScenario(defaults);
(await events.EventLog.Append(source, new Credited(5))).ShouldBeSuccessful();
}
}
}

View C# snippet source on GitHub

Create independent defaults/options for independent runs. Each scenario store captures its original recipes; changing shared options afterward does not rewrite an existing store. Do not mutate Defaults.Instance or the default artifact provider to configure a run.

XUnit integration options are host-lifetime, matching ChronicleOptions: configure explicit artifacts before the fixture host/client/store is created. Reuse of that host keeps its first snapshot, even after rediscovery. Separate fixture hosts have isolated snapshots. The existing sharing boundary is the closed ChronicleClientFixture<TChronicleFixture> family; derived scenarios in the same family share its host. A fixture that overrides ChronicleFixture.UsesPerContextMongoDBHost to return true starts a fresh host for each test context, so each context captures its own snapshot. AutoDiscoverArtifacts = false disables assembly discovery, not explicit nominations or automatic client registration. It is different from AutoDiscoverAndRegister = false, which requires manual discovery and registration.

See the .NET explicit registration reference for peer conflicts, migration binding and custom-provider limits.

When code under test distinguishes stores by identity, construct EventStoreForTesting(serviceProvider, artifacts, options, time, eventStore, @namespace, capabilities: null) or Defaults(artifacts, serviceProvider, options, time, eventStore, @namespace). The eventStore and @namespace arguments are EventStoreName and EventStoreNamespaceName. EventStoreForTesting requires a ScenarioTime; Defaults accepts a null clock.

The resulting store’s Name, Namespace, event log and unit-of-work operations use that identity. Pass those defaults to ReadModelScenario, ReactorScenario or OccurrenceScenario to give the scenario’s store the same identity. A reactor scenario supplied with an explicit event store uses that store’s identity instead, including in delivered event contexts. EventScenario keeps its own identity parameters and does not inherit the defaults’ store identity.

Each store instance has isolated in-memory storage, even when identities match. Existing EventStoreForTesting and Defaults overloads still create their store as testing in namespace default.

GuideDescription
EventsTest event appends and constraint behavior in-process using EventScenario
Read ModelsTest projections and reducers in-process using ReadModelScenario<TReadModel>
ReactorsTest reactor side-effects in-process using ReactorScenario<TReactor>
Occurrences and receiptsDrive leased trigger deliveries, retry time and commit-then-drop-reply recovery
Event append collectionIntegration testing: capture every appended event in a scoped collector with IEventAppendCollection
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

The read-model logic runs in-process — a projection or reducer, depending on the client — and the assertion stays close to the domain: given the AuthorRegistered fact, the Author read model should contain the name the screen needs.

The .NET in-process scenario helpers run the real projection and reducer engines, constraint evaluation, and reactor dispatch on Chronicle’s production in-memory storage. Each scenario owns an isolated store shared by its event log, named sequences, and in-process services. They bypass gRPC and the observer lifecycle, so they remain fast without proving network delivery or persistent storage behavior.

When a .NET test needs to prove behavior across that boundary — that a projection’s sink writes actually landed in MongoDB, that a reactor registered its observer subscription with a running kernel, that change streams fired and advanced the checkpoint — reach for the integration fixtures and IEventAppendCollection instead. Those exercise the full client-to-kernel-to-storage stack.

For a worked example of IEventAppendCollection and the integration fixtures, see Event append collection. For the full slice testing story — how /chronicle/scenarios/test-a-slice/ fits with the in-process helpers and the integration boundary — see Test a slice.

For the bigger testing model across Specifications, Arc, Chronicle, and full stack slices, see Testing with Cratis.