EventScenario
EventScenario is a lightweight, in-process test utility for exercising code that appends events to an IEventLog or IEventSequence. It runs entirely in memory with no Chronicle server, database, or network required.
In a Cratis Specification, EventScenario.Given is the given part of the spec, EventScenario.When is the when — it appends the acted event(s) and returns the AppendResult — and the assertions on that result or the event sequence are the then. You can still call EventLog.Append / EventSequence.Append directly when you need the other overload parameters.
Why use EventScenario
Section titled “Why use EventScenario”Chronicle integration tests against a live server are accurate but slow and require infrastructure. EventScenario lets you verify that your domain code appends the right events, handles constraint violations correctly, and reacts to pre-seeded state — all in a fast, isolated, and infrastructure-free way.
Installation
Section titled “Installation”EventScenario is in the Cratis.Chronicle.Testing NuGet package:
dotnet add package Cratis.Specifications.XUnitdotnet add package Cratis.Chronicle.TestingBasic usage
Section titled “Basic usage”using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
[EventType]public record TestingScenarioAuthorRegistered(string Name);
[EventType]public record TestingScenarioBookAdded(string Title);
public static class TestingScenarioBasic{ public static async Task Run() { var scenario = new EventScenario(); var result = await scenario.EventLog.Append(EventSourceId.New(), new TestingScenarioAuthorRegistered("John Doe")); result.ShouldBeSuccessful(); }}EventLog and EventSequence are backed by the same in-memory store. Use EventLog for domain tests (it maps to EventSequenceId.Log). Use EventSequence when you need a generic IEventSequence reference.
Pre-seeding state with Given
Section titled “Pre-seeding state with Given”Use the fluent Given builder to put the in-memory event log into a known state before running the act phase:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
public static class TestingScenarioGiven{ public static async Task Run() { var authorId = EventSourceId.New(); var scenario = new EventScenario();
await scenario.Given .ForEventSource(authorId) .Events(new TestingScenarioAuthorRegistered("John Doe"), new TestingScenarioBookAdded("Clean Code"));
var result = await scenario.EventLog.Append(authorId, new TestingScenarioBookAdded("The Pragmatic Programmer")); result.ShouldBeSuccessful(); }}Chain multiple ForEventSource calls to seed events for different event sources in the same scenario:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
public static class TestingScenarioGivenMultipleSources{ public static async Task Run() { var author1Id = EventSourceId.New(); var author2Id = EventSourceId.New(); var scenario = new EventScenario();
await scenario.Given .ForEventSource(author1Id) .Events(new TestingScenarioAuthorRegistered("Jane Smith"));
await scenario.Given .ForEventSource(author2Id) .Events(new TestingScenarioAuthorRegistered("John Doe")); }}Rules:
- Call
Givenbefore the act phase — seeded events get monotonically increasing sequence numbers. - Do not put the act under test inside
Given; only pre-existing state goes here.
Acting with When
Section titled “Acting with When”When mirrors Given: where Given seeds pre-existing state, When performs the act under test. It appends the event(s) through the same in-memory kernel grain and returns the resulting AppendResult — the same “the act returns its result” shape as CommandScenario.Execute — so a constraint or append spec reads with Given / When / then symmetry without binding the raw Append overload by hand:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;using Cratis.Chronicle.Testing.EventSequences;
[EventType]public record TestingScenarioWhenAuthorRegistered([property: Unique(name: "TestingScenarioUniqueAuthorName")] string Name);
public static class TestingScenarioWhen{ public static async Task Run() { var existingAuthorId = EventSourceId.New(); var newAuthorId = EventSourceId.New(); var scenario = new EventScenario();
// Given: an author with this name is already registered await scenario.Given .ForEventSource(existingAuthorId) .Events(new TestingScenarioWhenAuthorRegistered("John Doe"));
// When: attempt to register the same name under a new event source — When returns the AppendResult var result = await scenario.When .ForEventSource(newAuthorId) .Events(new TestingScenarioWhenAuthorRegistered("John Doe"));
// Then: assert on the returned result result.ShouldHaveConstraintViolation("TestingScenarioUniqueAuthorName"); }}The returned value is an AppendResult, so every append assertion is available on it. When you supply more than one event to a single When, each is appended in order and the result of the final append is returned.
Testing AppendMany
Section titled “Testing AppendMany”AppendMany appends a collection of events in one call and returns an AppendManyResult:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
[EventType]public record TestingScenarioItemAddedToCart(string ItemId);
[EventType]public record TestingScenarioItemQuantityAdjusted(string ItemId, int Quantity);
public static class TestingScenarioAppendMany{ public static async Task Run() { var cartId = EventSourceId.New(); var itemId1 = "item-1"; var itemId2 = "item-2"; var scenario = new EventScenario();
var result = await scenario.EventLog.AppendMany(cartId, [ new TestingScenarioItemAddedToCart(itemId1), new TestingScenarioItemAddedToCart(itemId2), new TestingScenarioItemQuantityAdjusted(itemId1, 3) ]); result.ShouldBeSuccessful(); }}Isolation
Section titled “Isolation”Create a new EventScenario instance per test to keep tests isolated. The in-memory store accumulates state across calls on the same instance. Each instance starts with an empty event log and sequence number zero.
Example: full test
Section titled “Example: full test”using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;using Xunit;
public class when_adding_a_book_to_an_author : Specification{ readonly EventSourceId _authorId = EventSourceId.New(); readonly EventScenario _scenario = new(); AppendResult _result = default!;
Task Establish() => _scenario.Given .ForEventSource(_authorId) .Events(new TestingScenarioAuthorRegistered("Jane Smith"));
async Task Because() => _result = await _scenario.EventLog.Append(_authorId, new TestingScenarioBookAdded("Clean Code"));
[Fact] void should_append_successfully() => _result.ShouldBeSuccessful();
[Fact] Task should_have_appended_book_added() => _scenario.EventLog.ShouldHaveAppendedEvent<TestingScenarioBookAdded>(new EventSequenceNumber(1));}For information on asserting the result of an append operation, see Assertions.