Event Sequence Assertions
After appending events through an EventScenario, you often need to verify what ended up in the event sequence itself — not just the result of the append call. The Cratis.Chronicle.Testing package provides Should* extension methods on IEventSequence and IEventLog for these assertions, as well as Should* extension methods on AppendedEventWithResult for asserting on collected events.
Event sequence assertion reference
Section titled “Event sequence assertion reference”These methods are available on any IEventSequence or IEventLog instance, including scenario.EventLog and scenario.EventSequence.
| Method | Asserts |
|---|---|
ShouldHaveTailSequenceNumber(expected) | Tail sequence number matches the expected value |
ShouldHaveAppendedEvent<TEvent>() | At least one event of the given type exists anywhere in the sequence |
ShouldHaveAppendedEvent<TEvent>(validator) | At least one event of the given type exists and passes the validator |
ShouldHaveAppendedEvent<TEvent>(predicate) | At least one event of the given type matches the predicate |
ShouldHaveAppendedEvent<TEvent>(eventSourceId) | At least one event of the given type exists for the event source |
ShouldHaveAppendedEvent<TEvent>(eventSourceId, validator) | At least one event of the given type exists for the event source and passes the validator |
ShouldHaveAppendedEvent<TEvent>(eventSourceId, predicate) | At least one event of the given type matches the predicate for the event source |
ShouldHaveAppendedEvent<TEvent>(sequenceNumber) | An event of the given type exists at the sequence number |
ShouldHaveAppendedEvent<TEvent>(sequenceNumber, validator) | An event of the given type exists at the sequence number and passes the validator |
ShouldHaveAppendedEvent<TEvent>(sequenceNumber, predicate) | An event of the given type at the sequence number matches the predicate |
ShouldHaveAppendedEvent<TEvent>(sequenceNumber, eventSourceId, validator) | An event of the given type exists at the sequence number for the given event source and passes the validator |
ShouldHaveAppendedEvent<TEvent>(sequenceNumber, eventSourceId, predicate) | An event of the given type at the sequence number for the given event source matches the predicate |
All methods are async and return Task. They throw EventSequenceAssertionException on failure with a descriptive message.
Verifying the tail sequence number
Section titled “Verifying the tail sequence number”The tail sequence number is the sequence number of the last event appended to the sequence. Use ShouldHaveTailSequenceNumber to verify the expected number of events were appended:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
[EventType]public record TestingSeqAssertAuthorRegistered(string Name);
[EventType]public record TestingSeqAssertBookAdded(string Title);
public static class TestingSeqAssertTailSequenceNumber{ public static async Task Run() { var scenario = new EventScenario(); var authorId = EventSourceId.New();
await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith")); await scenario.EventLog.Append(authorId, new TestingSeqAssertBookAdded("Clean Code"));
await scenario.EventLog.ShouldHaveTailSequenceNumber(1); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.testing.EventScenarioimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqTailAuthorRegistered(val name: String)
@EventTypedata class TestingSeqTailBookAdded(val title: String)
class TailSequenceNumberTests {
@Test fun `the tail sequence number is the position of the last event appended`() = runBlocking { val scenario = EventScenario() val authorId = "author-1"
scenario.eventLog.append(authorId, TestingSeqTailAuthorRegistered("Jane Smith")) scenario.eventLog.append(authorId, TestingSeqTailBookAdded("Clean Code"))
assertEquals(1L, scenario.eventLog.getTailSequenceNumber().value) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestingEventsEventSequenceAssertionsTailSequenceNumber {
@EventType record AuthorRegistered(String name) { }
@EventType record BookAdded(String title) { }
@Test void theTailSequenceNumberIsThePositionOfTheLastEventAppended() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var authorId = "author-1";
eventLog.append(authorId, new AuthorRegistered("Jane Smith")); eventLog.append(authorId, new BookAdded("Clean Code"));
assertEquals(1L, eventLog.getTailSequenceNumber()); }}defmodule MyApp.Events.TestingSeqAssertTailAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-tail-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertTailBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-tail-book-added"
defstruct [:title]end
defmodule MyApp.TestingSeqAssertTailSequenceNumberTest do use Chronicle.Testing.Case, event_types: [MyApp.Events.TestingSeqAssertTailAuthorRegistered, MyApp.Events.TestingSeqAssertTailBookAdded]
alias Chronicle.EventSequences.EventLog alias MyApp.Events.{TestingSeqAssertTailAuthorRegistered, TestingSeqAssertTailBookAdded}
test "the tail is the sequence number of the last appended event", %{chronicle: client} do author_id = "author-#{System.unique_integer([:positive])}"
:ok = EventLog.append(author_id, %TestingSeqAssertTailAuthorRegistered{name: "Jane Smith"}, client: client) :ok = EventLog.append(author_id, %TestingSeqAssertTailBookAdded{title: "Clean Code"}, client: client)
# Sequence numbers are zero-based: two events put the tail at 1. assert {:ok, 1} = EventLog.get_tail_sequence_number(nil, client: client) endendTypeScript does not support this workflow yet.Sequence numbers are zero-based, so the first event has sequence number 0 and the second has 1. The special value EventSequenceNumber.Unavailable indicates no events have been appended.
Verifying an appended event by type
Section titled “Verifying an appended event by type”Use ShouldHaveAppendedEvent<TEvent> without a sequence number to verify that at least one event of the expected type was appended anywhere in the sequence:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
public static class TestingSeqAssertAppendedEventByType{ public static async Task Run() { var scenario = new EventScenario(); var authorId = EventSourceId.New();
await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith")); await scenario.EventLog.Append(authorId, new TestingSeqAssertBookAdded("Clean Code"));
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(); await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertBookAdded>(); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.testing.EventScenarioimport io.cratis.chronicle.testing.shouldHaveAppendedimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqByTypeAuthorRegistered(val name: String)
@EventTypedata class TestingSeqByTypeBookAdded(val title: String)
class AppendedEventByTypeTests {
@Test fun `at least one event of a type was appended somewhere in the sequence`() = runBlocking { val scenario = EventScenario() val authorId = "author-1"
scenario.eventLog.append(authorId, TestingSeqByTypeAuthorRegistered("Jane Smith")) scenario.eventLog.append(authorId, TestingSeqByTypeBookAdded("Clean Code"))
scenario.shouldHaveAppended<TestingSeqByTypeAuthorRegistered>() scenario.shouldHaveAppended<TestingSeqByTypeBookAdded>() }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
class TestingEventsEventSequenceAssertionsAppendedEventByType {
@EventType record AuthorRegistered(String name) { }
@EventType record BookAdded(String title) { }
@Test void atLeastOneEventOfATypeWasAppendedSomewhereInTheSequence() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var authorId = "author-1";
eventLog.append(authorId, new AuthorRegistered("Jane Smith")); eventLog.append(authorId, new BookAdded("Clean Code"));
assertFalse(eventLog.getForEventSourceIdAndEventTypes(authorId, AuthorRegistered.class).isEmpty()); assertFalse(eventLog.getForEventSourceIdAndEventTypes(authorId, BookAdded.class).isEmpty()); }}defmodule MyApp.Events.TestingSeqAssertByTypeAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-by-type-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertByTypeBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-by-type-book-added"
defstruct [:title]end
defmodule MyApp.TestingSeqAssertAppendedEventByTypeTest do use Chronicle.Testing.Case, event_types: [MyApp.Events.TestingSeqAssertByTypeAuthorRegistered, MyApp.Events.TestingSeqAssertByTypeBookAdded]
import Chronicle.Testing.Assertions
alias Chronicle.EventSequences.EventLog alias MyApp.Events.{TestingSeqAssertByTypeAuthorRegistered, TestingSeqAssertByTypeBookAdded}
test "an event of each type was appended somewhere in the sequence", %{chronicle: client} do author_id = "author-#{System.unique_integer([:positive])}"
:ok = EventLog.append(author_id, %TestingSeqAssertByTypeAuthorRegistered{name: "Jane Smith"}, client: client) :ok = EventLog.append(author_id, %TestingSeqAssertByTypeBookAdded{title: "Clean Code"}, client: client)
assert_appended(client, TestingSeqAssertByTypeAuthorRegistered) assert_appended(client, TestingSeqAssertByTypeBookAdded) endendTypeScript does not support this workflow yet.When you know the exact position, pass a sequence number to assert at a specific location:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
public static class TestingSeqAssertAppendedEventAtPosition{ public static async Task Run(EventScenario scenario) { await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(0); await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertBookAdded>(1); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.json.chronicleGsonimport io.cratis.chronicle.testing.EventScenarioimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqAtPositionAuthorRegistered(val name: String)
@EventTypedata class TestingSeqAtPositionBookAdded(val title: String)
class AppendedEventAtPositionTests {
@Test fun `an event exists at a specific position in the sequence`() = runBlocking { val scenario = EventScenario() val authorId = "author-1"
scenario.eventLog.append(authorId, TestingSeqAtPositionAuthorRegistered("Jane Smith")) scenario.eventLog.append(authorId, TestingSeqAtPositionBookAdded("Clean Code"))
val events = scenario.eventLog.events val author = chronicleGson.fromJson(events[0].content, TestingSeqAtPositionAuthorRegistered::class.java) val book = chronicleGson.fromJson(events[1].content, TestingSeqAtPositionBookAdded::class.java)
assertEquals("Jane Smith", author.name) assertEquals("Clean Code", book.title) }}import com.google.gson.Gson;
import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestingEventsEventSequenceAssertionsAppendedEventAtPosition {
@EventType record AuthorRegistered(String name) { }
@EventType record BookAdded(String title) { }
@Test void anEventExistsAtASpecificPositionInTheSequence() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var authorId = "author-1"; var gson = new Gson();
eventLog.append(authorId, new AuthorRegistered("Jane Smith")); eventLog.append(authorId, new BookAdded("Clean Code"));
var events = scenario.getEventLog().getEvents(); var author = gson.fromJson(events.get(0).getContent(), AuthorRegistered.class); var book = gson.fromJson(events.get(1).getContent(), BookAdded.class);
assertEquals("Jane Smith", author.name()); assertEquals("Clean Code", book.title()); }}defmodule MyApp.Events.TestingSeqAssertPositionAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-position-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertPositionBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-position-book-added"
defstruct [:title]end
defmodule MyApp.TestingSeqAssertAppendedEventAtPositionTest do use Chronicle.Testing.Case, event_types: [MyApp.Events.TestingSeqAssertPositionAuthorRegistered, MyApp.Events.TestingSeqAssertPositionBookAdded]
alias Chronicle.EventSequences.EventLog alias MyApp.Events.{TestingSeqAssertPositionAuthorRegistered, TestingSeqAssertPositionBookAdded}
test "each event sits at the sequence number it was appended at", %{chronicle: client} do author_id = "author-#{System.unique_integer([:positive])}"
:ok = EventLog.append(author_id, %TestingSeqAssertPositionAuthorRegistered{name: "Jane Smith"}, client: client) :ok = EventLog.append(author_id, %TestingSeqAssertPositionBookAdded{title: "Clean Code"}, client: client)
appended = Chronicle.Testing.appended_events(client, author_id)
assert [ %{event: %TestingSeqAssertPositionAuthorRegistered{}, context: %{sequence_number: 0}}, %{event: %TestingSeqAssertPositionBookAdded{}, context: %{sequence_number: 1}} ] = appended endendTypeScript does not support this workflow yet.Verifying event content
Section titled “Verifying event content”Pass a validator action to inspect the event content. The assertion fails if the event is not of the expected type or if the validator throws:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;
public static class TestingSeqAssertValidator{ public static async Task Run() { var scenario = new EventScenario(); var authorId = EventSourceId.New();
await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith"));
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(0, author => author.Name.ShouldEqual("Jane Smith")); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.json.chronicleGsonimport io.cratis.chronicle.testing.EventScenarioimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqValidatorAuthorRegistered(val name: String)
class ValidatorTests {
@Test fun `the event at a known position carries the expected content`() = runBlocking { val scenario = EventScenario()
scenario.eventLog.append("author-1", TestingSeqValidatorAuthorRegistered("Jane Smith"))
val author = chronicleGson.fromJson(scenario.eventLog.events[0].content, TestingSeqValidatorAuthorRegistered::class.java) assertEquals("Jane Smith", author.name) }}import com.google.gson.Gson;
import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestingEventsEventSequenceAssertionsValidator {
@EventType record AuthorRegistered(String name) { }
@Test void theEventAtAKnownPositionCarriesTheExpectedContent() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var gson = new Gson();
eventLog.append("author-1", new AuthorRegistered("Jane Smith"));
var author = gson.fromJson(scenario.getEventLog().getEvents().get(0).getContent(), AuthorRegistered.class);
assertEquals("Jane Smith", author.name()); }}defmodule MyApp.Events.TestingSeqAssertValidatorAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-validator-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertValidatorBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-validator-book-added"
defstruct [:title]end
defmodule MyApp.TestingSeqAssertValidatorTest do use Chronicle.Testing.Case, event_types: [MyApp.Events.TestingSeqAssertValidatorAuthorRegistered]
alias Chronicle.EventSequences.EventLog alias MyApp.Events.TestingSeqAssertValidatorAuthorRegistered
test "the event at sequence number 0 carries the expected content", %{chronicle: client} do author_id = "author-#{System.unique_integer([:positive])}"
:ok = EventLog.append(author_id, %TestingSeqAssertValidatorAuthorRegistered{name: "Jane Smith"}, client: client)
[first | _] = Chronicle.Testing.appended_events(client, author_id)
# Matching on the decoded event is the validator: it fails unless the type and content match. assert %{event: %TestingSeqAssertValidatorAuthorRegistered{name: "Jane Smith"}, context: %{sequence_number: 0}} = first endendTypeScript does not support this workflow yet.Without a sequence number, the validator runs against the first event of the matching type:
using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;
public static class TestingSeqAssertValidatorNoSequence{ public static Task Run(EventScenario scenario) => scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(author => author.Name.ShouldEqual("Jane Smith"));}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.testing.EventScenarioimport io.cratis.chronicle.testing.shouldHaveAppendedimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqValidatorNoSeqAuthorRegistered(val name: String)
class ValidatorNoSequenceTests {
@Test fun `the first matching event anywhere in the sequence carries the expected content`() = runBlocking { val scenario = EventScenario()
scenario.eventLog.append("author-1", TestingSeqValidatorNoSeqAuthorRegistered("Jane Smith"))
val author = scenario.shouldHaveAppended<TestingSeqValidatorNoSeqAuthorRegistered> { it.name == "Jane Smith" } assertEquals("Jane Smith", author.name) }}import com.google.gson.Gson;
import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestingEventsEventSequenceAssertionsValidatorNoSequence {
@EventType record AuthorRegistered(String name) { }
@Test void theFirstMatchingEventAnywhereInTheSequenceCarriesTheExpectedContent() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var gson = new Gson();
eventLog.append("author-1", new AuthorRegistered("Jane Smith"));
var author = gson.fromJson(scenario.getEventLog().getEvents().get(0).getContent(), AuthorRegistered.class);
assertEquals("Jane Smith", author.name()); }}defmodule MyApp.Events.TestingSeqAssertValidatorAnyAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-validator-any-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertValidatorAnyBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-validator-any-book-added"
defstruct [:title]end
defmodule MyApp.TestingSeqAssertValidatorNoSequenceTest do use Chronicle.Testing.Case, event_types: [MyApp.Events.TestingSeqAssertValidatorAnyAuthorRegistered, MyApp.Events.TestingSeqAssertValidatorAnyBookAdded]
alias Chronicle.EventSequences.EventLog alias MyApp.Events.{TestingSeqAssertValidatorAnyAuthorRegistered, TestingSeqAssertValidatorAnyBookAdded}
test "the first event of the matching type carries the expected content", %{chronicle: client} do author_id = "author-#{System.unique_integer([:positive])}"
:ok = EventLog.append(author_id, %TestingSeqAssertValidatorAnyBookAdded{title: "Clean Code"}, client: client) :ok = EventLog.append(author_id, %TestingSeqAssertValidatorAnyAuthorRegistered{name: "Jane Smith"}, client: client)
# No sequence number: take the first event of the matching type. first = client |> Chronicle.Testing.appended_events(author_id) |> Enum.find(&match?(%{event: %TestingSeqAssertValidatorAnyAuthorRegistered{}}, &1))
assert %{event: %TestingSeqAssertValidatorAnyAuthorRegistered{name: "Jane Smith"}} = first endendTypeScript does not support this workflow yet.Verifying with a predicate
Section titled “Verifying with a predicate”Pass a Func<TEvent, bool> predicate when you only need to check whether the event satisfies a condition. The assertion fails if no event of the expected type returns true from the predicate:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
public static class TestingSeqAssertPredicate{ public static async Task Run() { var scenario = new EventScenario(); var authorId = EventSourceId.New();
await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith"));
// Without sequence number — finds any matching event await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>( author => author.Name == "Jane Smith");
// At a specific sequence number await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(0, author => author.Name == "Jane Smith"); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.testing.EventScenarioimport io.cratis.chronicle.testing.shouldHaveAppendedimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqPredicateAuthorRegistered(val name: String)
class PredicateTests {
@Test fun `an appended event satisfies a condition`() = runBlocking { val scenario = EventScenario()
scenario.eventLog.append("author-1", TestingSeqPredicateAuthorRegistered("Jane Smith"))
scenario.shouldHaveAppended<TestingSeqPredicateAuthorRegistered> { it.name == "Jane Smith" } }}import com.google.gson.Gson;
import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TestingEventsEventSequenceAssertionsPredicate {
@EventType record AuthorRegistered(String name) { }
@Test void anAppendedEventSatisfiesACondition() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var gson = new Gson();
eventLog.append("author-1", new AuthorRegistered("Jane Smith"));
var matches = scenario.getEventLog().getEvents().stream() .map(appended -> gson.fromJson(appended.getContent(), AuthorRegistered.class)) .anyMatch(author -> author.name().equals("Jane Smith"));
assertTrue(matches); }}defmodule MyApp.Events.TestingSeqAssertPredicateAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-predicate-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertPredicateBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-predicate-book-added"
defstruct [:title]end
defmodule MyApp.TestingSeqAssertPredicateTest do use Chronicle.Testing.Case, event_types: [MyApp.Events.TestingSeqAssertPredicateAuthorRegistered, MyApp.Events.TestingSeqAssertPredicateBookAdded]
import Chronicle.Testing.Assertions
alias Chronicle.EventSequences.EventLog alias MyApp.Events.TestingSeqAssertPredicateAuthorRegistered
test "an event satisfies a predicate", %{chronicle: client} do author_id = "author-#{System.unique_integer([:positive])}"
:ok = EventLog.append(author_id, %TestingSeqAssertPredicateAuthorRegistered{name: "Jane Smith"}, client: client)
# The predicate is called with every appended event, so it answers false for other types. jane? = fn %TestingSeqAssertPredicateAuthorRegistered{name: name} -> name == "Jane Smith" _other -> false end
# Without a sequence number — any matching event assert_appended(client, jane?)
# At a specific sequence number [first | _] = Chronicle.Testing.appended_events(client, author_id) assert first.context.sequence_number == 0 assert jane?.(first.event) endendTypeScript does not support this workflow yet.Verifying event content for a specific event source
Section titled “Verifying event content for a specific event source”When events for multiple event sources exist in the same sequence, filter by event source to avoid ambiguity. All event source overloads support both Action<TEvent> validators and Func<TEvent, bool> predicates:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;
public static class TestingSeqAssertByEventSource{ public static async Task Run() { var scenario = new EventScenario(); var author1 = EventSourceId.New(); var author2 = EventSourceId.New();
await scenario.EventLog.Append(author1, new TestingSeqAssertAuthorRegistered("Jane Smith")); await scenario.EventLog.Append(author2, new TestingSeqAssertAuthorRegistered("John Doe"));
// With sequence number and validator await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(0, author1, author => author.Name.ShouldEqual("Jane Smith"));
// Without sequence number — finds any matching event for the event source await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(author2, author => author.Name.ShouldEqual("John Doe"));
// With a predicate instead of a validator await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>( author1, author => author.Name == "Jane Smith"); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.testing.EventScenarioimport io.cratis.chronicle.testing.shouldHaveAppendedimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqByEventSourceAuthorRegistered(val name: String)
class ByEventSourceTests {
@Test fun `an appended event is scoped to its own event source`() = runBlocking { val scenario = EventScenario() val author1 = "author-1" val author2 = "author-2"
scenario.eventLog.append(author1, TestingSeqByEventSourceAuthorRegistered("Jane Smith")) scenario.eventLog.append(author2, TestingSeqByEventSourceAuthorRegistered("John Doe"))
val first = scenario.shouldHaveAppended<TestingSeqByEventSourceAuthorRegistered>(author1) { it.name == "Jane Smith" } val second = scenario.shouldHaveAppended<TestingSeqByEventSourceAuthorRegistered>(author2) { it.name == "John Doe" }
assertEquals("Jane Smith", first.name) assertEquals("John Doe", second.name) }}import com.google.gson.Gson;
import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestingEventsEventSequenceAssertionsByEventSource {
@EventType record AuthorRegistered(String name) { }
@Test void anAppendedEventIsScopedToItsOwnEventSource() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var author1 = "author-1"; var author2 = "author-2"; var gson = new Gson();
eventLog.append(author1, new AuthorRegistered("Jane Smith")); eventLog.append(author2, new AuthorRegistered("John Doe"));
var first = gson.fromJson( eventLog.getForEventSourceIdAndEventTypes(author1, AuthorRegistered.class).get(0).getContent(), AuthorRegistered.class); var second = gson.fromJson( eventLog.getForEventSourceIdAndEventTypes(author2, AuthorRegistered.class).get(0).getContent(), AuthorRegistered.class);
assertEquals("Jane Smith", first.name()); assertEquals("John Doe", second.name()); }}defmodule MyApp.Events.TestingSeqAssertSourceAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-source-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertSourceBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-source-book-added"
defstruct [:title]end
defmodule MyApp.TestingSeqAssertByEventSourceTest do use Chronicle.Testing.Case, event_types: [MyApp.Events.TestingSeqAssertSourceAuthorRegistered]
import Chronicle.Testing.Assertions
alias Chronicle.EventSequences.EventLog alias MyApp.Events.TestingSeqAssertSourceAuthorRegistered
test "assertions can be narrowed to one event source", %{chronicle: client} do author1 = "author-#{System.unique_integer([:positive])}" author2 = "author-#{System.unique_integer([:positive])}"
:ok = EventLog.append(author1, %TestingSeqAssertSourceAuthorRegistered{name: "Jane Smith"}, client: client) :ok = EventLog.append(author2, %TestingSeqAssertSourceAuthorRegistered{name: "John Doe"}, client: client)
# With a sequence number and the exact content [first] = Chronicle.Testing.appended_events(client, author1)
assert %{ event: %TestingSeqAssertSourceAuthorRegistered{name: "Jane Smith"}, context: %{sequence_number: 0, event_source_id: ^author1} } = first
# Without a sequence number — any matching event for the event source assert_appended(client, %TestingSeqAssertSourceAuthorRegistered{name: "John Doe"}, event_source_id: author2)
# With a predicate instead of the exact content assert_appended(client, &match?(%TestingSeqAssertSourceAuthorRegistered{name: "Jane Smith"}, &1), event_source_id: author1 ) endendTypeScript does not support this workflow yet.AppendedEventWithResult assertions
Section titled “AppendedEventWithResult assertions”When using IEventAppendCollection to collect events (see Event Append Collection), each captured entry is an AppendedEventWithResult. The testing package provides Should* extensions for asserting directly on these entries.
Result assertions
Section titled “Result assertions”All append result assertions are available directly on AppendedEventWithResult — they delegate to the inner Result:
| Method | Asserts |
|---|---|
ShouldBeSuccessful() | Operation succeeded with no violations or errors |
ShouldBeFailed() | Operation failed (any violation or error) |
ShouldHaveConstraintViolations() | At least one constraint violation is present |
ShouldNotHaveConstraintViolations() | No constraint violations are present |
ShouldHaveConstraintViolationFor(name) | A violation for the named constraint is present |
ShouldHaveConcurrencyViolations() | At least one concurrency violation is present |
ShouldNotHaveConcurrencyViolations() | No concurrency violations are present |
ShouldHaveErrors() | At least one error is present |
ShouldNotHaveErrors() | No errors are present |
Event assertions
Section titled “Event assertions”| Method | Asserts |
|---|---|
ShouldHaveEvent<TEvent>(validate?) | Event content is of the given type; optional validator inspects the content |
ShouldBeForEventSource(eventSourceId) | Event was appended for the given event source |
Example: asserting on a collected event
Section titled “Example: asserting on a collected event”using System.Linq;using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;
public static class TestingSeqAssertAppendedEventWithResult{ public static async Task Run() { var scenario = new EventScenario(); var authorId = EventSourceId.New();
var result = await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith")); var appendedEvents = await scenario.EventLog.GetFromSequenceNumber(EventSequenceNumber.First, authorId); var collected = new AppendedEventWithResult(appendedEvents.Last(), result);
collected.ShouldBeSuccessful(); collected.ShouldHaveEvent<TestingSeqAssertAuthorRegistered>(author => author.Name.ShouldEqual("Jane Smith")); collected.ShouldBeForEventSource(authorId); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.eventSequences.AppendedEventWithResultimport io.cratis.chronicle.testing.EventScenarioimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Assertions.assertTrueimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqWithResultAuthorRegistered(val name: String)
class AppendedEventWithResultTests {
@Test fun `pairing an appended event with its append result`() = runBlocking { val scenario = EventScenario() val authorId = "author-1" val event = TestingSeqWithResultAuthorRegistered("Jane Smith")
val result = scenario.eventLog.append(authorId, event) val context = scenario.eventLog.events.last().context val collected = AppendedEventWithResult(context, event, result)
assertTrue(collected.result.isSuccess) assertEquals("Jane Smith", (collected.event as TestingSeqWithResultAuthorRegistered).name) assertEquals(authorId, collected.context.eventSourceId) }}import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.eventSequences.AppendedEventWithResult;import io.cratis.chronicle.eventSequences.AppendResult;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertTrue;
class TestingEventsEventSequenceAssertionsAppendedEventWithResult {
@EventType record AuthorRegistered(String name) { }
@Test void pairingAnAppendedEventWithItsAppendResult() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var authorId = "author-1"; var event = new AuthorRegistered("Jane Smith");
AppendResult result = eventLog.append(authorId, event); var events = scenario.getEventLog().getEvents(); var context = events.get(events.size() - 1).getContext(); var collected = new AppendedEventWithResult(context, event, result);
assertTrue(collected.getResult().isSuccess()); assertEquals("Jane Smith", ((AuthorRegistered) collected.getEvent()).name()); assertEquals(authorId, collected.getContext().getEventSourceId()); }}defmodule MyApp.Events.TestingSeqAssertResultAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-result-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertResultBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-result-book-added"
defstruct [:title]end
defmodule MyApp.TestingSeqAssertAppendedEventWithResultTest do use Chronicle.Testing.Case, event_types: [MyApp.Events.TestingSeqAssertResultAuthorRegistered]
alias Chronicle.EventSequences.EventLog alias MyApp.Events.TestingSeqAssertResultAuthorRegistered
test "the append result and the stored event can be checked together", %{chronicle: client} do author_id = "author-#{System.unique_integer([:positive])}"
{:ok, result} = EventLog.append_with_result(author_id, %TestingSeqAssertResultAuthorRegistered{name: "Jane Smith"}, client: client)
[appended] = Chronicle.Testing.appended_events(client, author_id)
assert result.success? assert %TestingSeqAssertResultAuthorRegistered{name: "Jane Smith"} = appended.event assert appended.context.event_source_id == author_id assert appended.context.sequence_number == result.sequence_number endendTypeScript does not support this workflow yet.Full example
Section titled “Full example”The following test pre-seeds state, appends two events, and then verifies both the tail sequence number and individual event content:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;using Xunit;
[EventType]public record TestingSeqAssertLibraryCreated(string Name);
public record TestingSeqAssertAuthorId(Guid Value) : EventSourceId<Guid>(Value){ public static TestingSeqAssertAuthorId New() => new(Guid.NewGuid());}
public class when_registering_an_author_and_adding_a_book{ readonly EventScenario _scenario = new();
[Fact] public async Task should_append_both_events_in_order() { var authorId = TestingSeqAssertAuthorId.New();
await _scenario.Given .ForEventSource(authorId) .Events(new TestingSeqAssertLibraryCreated("Main Library"));
await _scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith")); await _scenario.EventLog.Append(authorId, new TestingSeqAssertBookAdded("Clean Code"));
// Tail includes the seeded event (0) plus the two appended events (1, 2) await _scenario.EventLog.ShouldHaveTailSequenceNumber(2);
await _scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(1, author => author.Name.ShouldEqual("Jane Smith")); await _scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertBookAdded>(2, book => book.Title.ShouldEqual("Clean Code")); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.testing.EventScenarioimport io.cratis.chronicle.testing.shouldHaveAppendedimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Test
@EventTypedata class TestingSeqFullExampleAuthorRegistered(val name: String)
@EventTypedata class TestingSeqFullExampleBookAdded(val title: String)
class RegisteringAnAuthorAndAddingABookTests {
@Test fun `both events land in order`() = runBlocking { val scenario = EventScenario() val authorId = "author-1"
scenario.eventLog.append(authorId, TestingSeqFullExampleAuthorRegistered("Jane Smith")) scenario.eventLog.append(authorId, TestingSeqFullExampleBookAdded("Clean Code"))
assertEquals(1L, scenario.eventLog.getTailSequenceNumber().value)
val author = scenario.shouldHaveAppended<TestingSeqFullExampleAuthorRegistered>(authorId) { it.name == "Jane Smith" } val book = scenario.shouldHaveAppended<TestingSeqFullExampleBookAdded>(authorId) { it.title == "Clean Code" }
assertEquals("Jane Smith", author.name) assertEquals("Clean Code", book.title) }}import com.google.gson.Gson;
import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventSequence;import io.cratis.chronicle.testing.EventScenario;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestingEventsEventSequenceAssertionsFullExample {
@EventType record AuthorRegistered(String name) { }
@EventType record BookAdded(String title) { }
@Test void bothEventsLandInOrder() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence()); var authorId = "author-1"; var gson = new Gson();
eventLog.append(authorId, new AuthorRegistered("Jane Smith")); eventLog.append(authorId, new BookAdded("Clean Code"));
assertEquals(1L, eventLog.getTailSequenceNumber());
var events = scenario.getEventLog().getEvents(); var author = gson.fromJson(events.get(0).getContent(), AuthorRegistered.class); var book = gson.fromJson(events.get(1).getContent(), BookAdded.class);
assertEquals("Jane Smith", author.name()); assertEquals("Clean Code", book.title()); }}defmodule MyApp.Events.TestingSeqAssertFullAuthorRegistered do use Chronicle.Events.EventType, id: "testing-seq-assert-full-author-registered"
defstruct [:name]end
defmodule MyApp.Events.TestingSeqAssertFullBookAdded do use Chronicle.Events.EventType, id: "testing-seq-assert-full-book-added"
defstruct [:title]end
defmodule MyApp.Events.TestingSeqAssertFullLibraryCreated do use Chronicle.Events.EventType, id: "testing-seq-assert-full-library-created"
defstruct [:name]end
defmodule MyApp.TestingSeqAssertFullExampleTest do use Chronicle.Testing.Case, event_types: [ MyApp.Events.TestingSeqAssertFullLibraryCreated, MyApp.Events.TestingSeqAssertFullAuthorRegistered, MyApp.Events.TestingSeqAssertFullBookAdded ]
alias Chronicle.EventSequences.EventLog alias MyApp.Events.{TestingSeqAssertFullAuthorRegistered, TestingSeqAssertFullBookAdded, TestingSeqAssertFullLibraryCreated}
test "registering an author and adding a book appends both events in order", %{chronicle: client} do author_id = "author-#{System.unique_integer([:positive])}"
# Given an event that is already in the sequence :ok = EventLog.append(author_id, %TestingSeqAssertFullLibraryCreated{name: "Main Library"}, client: client)
:ok = EventLog.append(author_id, %TestingSeqAssertFullAuthorRegistered{name: "Jane Smith"}, client: client) :ok = EventLog.append(author_id, %TestingSeqAssertFullBookAdded{title: "Clean Code"}, client: client)
# The tail includes the seeded event (0) plus the two appended events (1, 2) assert {:ok, 2} = EventLog.get_tail_sequence_number(nil, client: client)
[_seeded, author, book] = Chronicle.Testing.appended_events(client, author_id)
assert %{event: %TestingSeqAssertFullAuthorRegistered{name: "Jane Smith"}, context: %{sequence_number: 1}} = author assert %{event: %TestingSeqAssertFullBookAdded{title: "Clean Code"}, context: %{sequence_number: 2}} = book endendTypeScript does not support this workflow yet.For asserting on the result of an individual append operation, see Append Assertions.