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();}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.TestSliceAppendBookAdded do use Chronicle.Events.EventType, id: "test-slice-append-book-added"
defstruct [:title, :isbn]
unique(:isbn, name: "TestSliceAppendUniqueIsbn")end
defmodule MyApp.TestSliceAppendTest do # Exercises the real client SDK against a running Chronicle event store, # so it's skipped here; remove the tag to run it against a live store. use ExUnit.Case, async: true @moduletag :skip
alias MyApp.Events.TestSliceAppendBookAdded
test "adding a book succeeds" do book_id = Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)
result = Chronicle.append(book_id, %TestSliceAppendBookAdded{ title: "The Pragmatic Programmer", isbn: "978-0135957059" })
assert result == :ok endendTypeScript does not support this workflow yet.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();}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.TestSliceConstraintBookAdded do use Chronicle.Events.EventType, id: "test-slice-constraint-book-added"
defstruct [:title, :isbn]
unique(:isbn, name: "TestSliceConstraintUniqueIsbn")end
defmodule MyApp.TestSliceConstraintTest do # Exercises the real client SDK against a running Chronicle event store, # so it's skipped here; remove the tag to run it against a live store. use ExUnit.Case, async: true @moduletag :skip
alias MyApp.Events.TestSliceConstraintBookAdded
test "adding a second book with an isbn already in use is rejected" do Chronicle.append( Base.encode16(:crypto.strong_rand_bytes(8), case: :lower), %TestSliceConstraintBookAdded{title: "The Pragmatic Programmer", isbn: "978-0135957059"} )
result = Chronicle.append( Base.encode16(:crypto.strong_rand_bytes(8), case: :lower), %TestSliceConstraintBookAdded{ title: "The Pragmatic Programmer, 2nd ed.", isbn: "978-0135957059" } )
assert {:error, {:constraint_violations, violations}} = result
assert Enum.any?(violations, fn violation -> Map.get(violation, :Name) == "TestSliceConstraintUniqueIsbn" or Map.get(violation, :name) == "TestSliceConstraintUniqueIsbn" end) endendTypeScript does not support this workflow yet.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");}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.TestSliceReadModelBookAdded do use Chronicle.Events.EventType, id: "test-slice-read-model-book-added"
defstruct [:title, :isbn]end
defmodule MyApp.Events.TestSliceReadModelBookBorrowed do use Chronicle.Events.EventType, id: "test-slice-read-model-book-borrowed"
defstruct [:borrowed_by]end
defmodule MyApp.ReadModels.TestSliceReadModelBook do use Chronicle.ReadModels.ReadModel
alias MyApp.Events.{TestSliceReadModelBookAdded, TestSliceReadModelBookBorrowed}
defstruct id: nil, title: nil, on_loan: false, borrowed_by: nil
from TestSliceReadModelBookAdded, set: [id: :event_source_id, title: :title, on_loan: false]
from TestSliceReadModelBookBorrowed, set: [on_loan: true, borrowed_by: :borrowed_by]end
defmodule MyApp.TestSliceReadModelTest do # Exercises the real client SDK against a running Chronicle event store, # so it's skipped here; remove the tag to run it against a live store. use ExUnit.Case, async: true @moduletag :skip
alias MyApp.Events.{TestSliceReadModelBookAdded, TestSliceReadModelBookBorrowed} alias MyApp.ReadModels.TestSliceReadModelBook
test "borrowing a book marks it on loan" do book_id = Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)
Chronicle.append(book_id, %TestSliceReadModelBookAdded{ title: "The Pragmatic Programmer", isbn: "978-0135957059" })
Chronicle.append(book_id, %TestSliceReadModelBookBorrowed{borrowed_by: "Ada Lovelace"})
book = Chronicle.read_model(TestSliceReadModelBook, book_id)
assert book.on_loan assert book.borrowed_by == "Ada Lovelace" endendTypeScript does not support this workflow yet.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.