Skip to content

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.

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.

EventScenario is in the Cratis.Chronicle.Testing NuGet package:

Terminal window
dotnet add package Cratis.Specifications.XUnit
dotnet add package Cratis.Chronicle.Testing
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.

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 Given before 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.

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.

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();
}
}

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.

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.