Test a slice
Goal: verify a feature actually works — the right events get appended (constraints included) and the read model derived from them comes out right — without standing up a Chronicle server or a database.
Real engines, in-memory storage
Section titled “Real engines, in-memory storage”The Cratis.Chronicle.Testing package gives you two scenario types that wire up the real client and kernel code paths — constraint validation, serialization, the projection and reducer engines. Only the storage layer is in-memory, so there’s no expensive infrastructure to spin up: the specs are lightweight to run, yet exercise the same engine code that runs in production.
dotnet add package Cratis.Chronicle.TestingThe examples below use the Cratis Specifications style (Establish/Because/should_), but the scenarios work with any test framework.
Test the events with EventScenario
Section titled “Test the events with EventScenario”EventScenario exercises the appending side. Seed pre-existing history with Given, append through EventLog exactly like production code does, and assert on the AppendResult:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;using Xunit;
public record TestSliceBookId(Guid Value) : EventSourceId<Guid>(Value){ public static TestSliceBookId New() => new(Guid.NewGuid());}
[EventType]public record TestSliceBookAdded(string Title, [property: Unique(name: "TestSliceUniqueIsbn")] string Isbn);
[EventType]public record TestSliceBookBorrowed(string BorrowedBy);
public class when_adding_a_book : Specification, IDisposable{ EventScenario _scenario = null!; AppendResult _result = null!;
void Establish() => _scenario = new EventScenario();
async Task Because() => _result = await _scenario.EventLog.Append( TestSliceBookId.New(), new TestSliceBookAdded("The Pragmatic Programmer", "978-0135957059"));
[Fact] void should_be_successful() => _result.ShouldBeSuccessful();
public void Dispose() => _scenario.Dispose();}Constraints are discovered automatically, the same way the real client discovers them. So given a unique-ISBN constraint (like the one in Enforce a unique value), seeding a book and appending a duplicate proves the rule fires:
using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;using Xunit;
public class and_the_isbn_already_exists : Specification, IDisposable{ EventScenario _scenario = null!; AppendResult _result = null!;
async Task Establish() { _scenario = new EventScenario(); await _scenario.Given .ForEventSource(TestSliceBookId.New()) .Events(new TestSliceBookAdded("The Pragmatic Programmer", "978-0135957059")); }
async Task Because() => _result = await _scenario.EventLog.Append( TestSliceBookId.New(), new TestSliceBookAdded("The Pragmatic Programmer, 2nd ed.", "978-0135957059"));
[Fact] void should_be_rejected() => _result.ShouldBeFailed(); [Fact] void should_report_the_violated_constraint() => _result.ShouldHaveConstraintViolationFor("TestSliceUniqueIsbn");
public void Dispose() => _scenario.Dispose();}Beyond ShouldBeSuccessful/ShouldBeFailed and ShouldHaveConstraintViolationFor, there are assertions for concurrency violations and errors — the full family is in Append result assertions. Create a fresh EventScenario per spec and dispose it; the in-memory log accumulates state across calls on the same instance.
Test the read model with ReadModelScenario
Section titled “Test the read model with ReadModelScenario”ReadModelScenario<TReadModel> exercises the deriving side: feed a history of events through the real projection or reducer engine and assert on the resulting instance. It auto-detects how the read model is built — reducer, fluent projection, or model-bound attributes:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;using Cratis.Chronicle.Testing.ReadModels;using Cratis.Specifications;using Xunit;
[FromEvent<TestSliceBookAdded>]public record TestSliceBook( [Key] Guid Id, string Title, [SetValue<TestSliceBookBorrowed>(true)] bool OnLoan, [SetFrom<TestSliceBookBorrowed>(nameof(TestSliceBookBorrowed.BorrowedBy))] string? BorrowedBy);
public class when_a_book_is_borrowed : Specification{ readonly TestSliceBookId _bookId = TestSliceBookId.New(); readonly ReadModelScenario<TestSliceBook> _scenario = new();
Task Because() => _scenario.Given .ForEventSource(_bookId) .Events( new TestSliceBookAdded("The Pragmatic Programmer", "978-0135957059"), new TestSliceBookBorrowed("Ada Lovelace"));
[Fact] void should_be_on_loan() => _scenario.Instance!.OnLoan.ShouldBeTrue(); [Fact] void should_record_the_borrower() => _scenario.Instance!.BorrowedBy.ShouldEqual("Ada Lovelace");}For a read model spec the event history is the act — Given supplies the input, Instance is the output. If the spec fails, your mapping ([SetFrom<T>]/[SetValue<T>] attributes or reducer logic) is wrong, because the engine applying it is the real one.
What this does and doesn’t cover
Section titled “What this does and doesn’t cover”Together the two scenarios prove the Chronicle slice: command logic appends the right facts, and the facts fold into the right state. They don’t exercise your HTTP surface or UI — that’s a concern for your application framework’s own testing story.
See also
Section titled “See also”- Testing — the full testing model, including
ReactorScenariofor reactor side effects. - EventScenario and ReadModelScenario — every option, including initial state and dependency injection.
- Enforce a unique value — the constraint the duplicate-ISBN spec exercises.