Testing
Chronicle provides lightweight, in-process test utilities that let you verify event appending behavior, read model projections, and reactor side-effects without a running server or database. Tests are fast, isolated, and require no infrastructure.
In Cratis 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.
Packages
Section titled “Packages”<PackageReference Include="Cratis.Specifications.XUnit" /><PackageReference Include="Cratis.Chronicle.Testing" />Use Cratis.Testing instead of Cratis.Chronicle.Testing when the same spec also tests Arc commands.
Topics
Section titled “Topics”| Guide | Description |
|---|---|
| Events | Test event appends and constraint behavior in-process using EventScenario |
| Read Models | Test projections and reducers in-process using ReadModelScenario<TReadModel> |
| Reactors | Test reactor side-effects in-process using ReactorScenario<TReactor> |
A specification-shaped example
Section titled “A specification-shaped example”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 TestingIndexAuthorRegistered(string Name);
[FromEvent<TestingIndexAuthorRegistered>]public record TestingIndexAuthor([Key] Guid Id, string Name);
public class when_projecting_a_registered_author : Specification{ readonly EventSourceId _authorId = EventSourceId.New(); readonly ReadModelScenario<TestingIndexAuthor> _scenario = new();
Task Because() => _scenario.Given .ForEventSource(_authorId) .Events(new TestingIndexAuthorRegistered("Jane Austen"));
[Fact] void should_set_the_author_name() => _scenario.Instance!.Name.ShouldEqual("Jane Austen");}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModelimport io.cratis.chronicle.testing.ReadModelScenarioimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Test
@EventTypedata class TestingIndexAuthorRegistered(val name: String)
@ReadModeldata class Author(val name: String = "")
@Reducerclass AuthorReducer { fun registered(event: TestingIndexAuthorRegistered) = Author(event.name)}
class WhenProjectingARegisteredAuthorTests {
@Test fun `the author read model carries the registered name`() = runBlocking { val scenario = ReadModelScenario<Author>(AuthorReducer())
val author = scenario.fold("author-1", TestingIndexAuthorRegistered("Jane Austen"))
assertEquals("Jane Austen", author!!.name) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestingExampleSpec {
@EventType record AuthorRegistered(String name) { }
@ReadModel record Author(String name) { }
@Reducer static class AuthorReducer { Author registered(AuthorRegistered event) { return new Author(event.name()); } }
@Test void theAuthorReadModelCarriesTheRegisteredName() { var reducer = new AuthorReducer();
var author = reducer.registered(new AuthorRegistered("Jane Austen"));
assertEquals("Jane Austen", author.name()); }}Elixir does not support this workflow yet.TypeScript does not support this workflow yet.The projection runs in-process, but the spec still reads like the domain: given the AuthorRegistered fact, the Author read model should contain the name the screen needs.
For the bigger testing model across Specifications, Arc, Chronicle, and full stack slices, see Testing with Cratis.