Append Assertions
Every Append and AppendMany call returns an IAppendResult. The Cratis.Chronicle.Testing package provides built-in Should* extension methods for asserting on these results.
Assertion reference
Section titled “Assertion reference”| Method | Asserts |
|---|---|
result.ShouldBeSuccessful() | Operation succeeded with no violations or errors |
result.ShouldBeFailed() | Operation failed (any violation or error) |
result.ShouldHaveConstraintViolations() | At least one constraint violation is present |
result.ShouldNotHaveConstraintViolations() | No constraint violations are present |
result.ShouldHaveConstraintViolationFor(name) | A violation for the named constraint is present |
result.ShouldHaveConstraintViolation(name) | Singular alias for ShouldHaveConstraintViolationFor(name) |
result.ShouldHaveConcurrencyViolations() | At least one concurrency violation is present |
result.ShouldNotHaveConcurrencyViolations() | No concurrency violations are present |
result.ShouldHaveErrors() | At least one error is present |
result.ShouldNotHaveErrors() | No errors are present |
All Should* methods throw AppendResultAssertionException on failure. The exception message describes all violations and errors found, making it easy to diagnose failures.
Happy path assertion
Section titled “Happy path assertion”using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;using Cratis.Chronicle.Testing.EventSequences;
[EventType]public record TestingAssertionsAuthorRegistered([property: Unique(name: "TestingAssertionsUniqueAuthorName")] string Name);
public static class TestingAssertionsHappyPath{ public static async Task Run() { var scenario = new EventScenario(); var authorId = EventSourceId.New(); var result = await scenario.EventLog.Append(authorId, new TestingAssertionsAuthorRegistered("Jane Smith")); result.ShouldBeSuccessful(); }}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.testing.EventScenarioimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertTrueimport org.junit.jupiter.api.Test
@EventTypedata class TestingAssertionsAuthorRegistered(val name: String)
class TestingAssertionsHappyPathTests {
@Test fun `appending an event succeeds`() = runBlocking { val scenario = EventScenario() val result = scenario.eventLog.append("author-1", TestingAssertionsAuthorRegistered("Jane Smith"))
assertTrue(result.isSuccess) }}import io.cratis.chronicle.events.EventType;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.assertTrue;
class TestingEventsAssertionsHappyPath {
@EventType record AuthorRegistered(String name) { }
@Test void appendingAnEventSucceeds() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence());
AppendResult result = eventLog.append("author-1", new AuthorRegistered("Jane Smith"));
assertTrue(result.isSuccess()); }}defmodule MyApp.Events.TestingAssertionsHappyPathAuthorRegistered do use Chronicle.Events.EventType, id: "testing-assertions-happy-path-author-registered"
defstruct [:name]end
defmodule MyApp.TestingAssertionsHappyPathTest do use ExUnit.Case, async: true
import Chronicle.Testing.Assertions
alias MyApp.Events.TestingAssertionsHappyPathAuthorRegistered
test "appending a new author succeeds" do {:ok, client} = Chronicle.Testing.start_supervised_client( event_types: [TestingAssertionsHappyPathAuthorRegistered] )
on_exit(fn -> Chronicle.Testing.stop_supervised_client(client) end)
author_id = "author-#{System.unique_integer([:positive])}"
result = Chronicle.EventSequences.EventLog.append( author_id, %TestingAssertionsHappyPathAuthorRegistered{name: "Jane Smith"}, client: client )
assert result == :ok assert_appended(client, TestingAssertionsHappyPathAuthorRegistered, event_source_id: author_id) endendTypeScript does not support this workflow yet.Failure assertions
Section titled “Failure assertions”using Cratis.Chronicle.Events;using Cratis.Chronicle.Testing.EventSequences;
public static class TestingAssertionsFailure{ public static async Task Run() { var scenario = new EventScenario(); var authorId = EventSourceId.New(); var newAuthorId = EventSourceId.New();
await scenario.Given .ForEventSource(authorId) .Events(new TestingAssertionsAuthorRegistered("Jane Smith"));
var result = await scenario.EventLog.Append(newAuthorId, new TestingAssertionsAuthorRegistered("Jane Smith")); result.ShouldBeFailed(); }}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.TestingAssertionsFailureAuthorRegistered do use Chronicle.Events.EventType, id: "testing-assertions-failure-author-registered"
defstruct [:name]
unique(:name, name: "TestingAssertionsFailureUniqueAuthorName")end
defmodule MyApp.TestingAssertionsFailureTest do use ExUnit.Case, async: true
alias MyApp.Events.TestingAssertionsFailureAuthorRegistered
test "appending a duplicate unique name fails" do {:ok, client} = Chronicle.Testing.start_supervised_client( event_types: [TestingAssertionsFailureAuthorRegistered] )
on_exit(fn -> Chronicle.Testing.stop_supervised_client(client) end)
:ok = Chronicle.EventSequences.EventLog.append( "author-#{System.unique_integer([:positive])}", %TestingAssertionsFailureAuthorRegistered{name: "Jane Smith"}, client: client )
result = Chronicle.EventSequences.EventLog.append( "author-#{System.unique_integer([:positive])}", %TestingAssertionsFailureAuthorRegistered{name: "Jane Smith"}, client: client )
assert {:error, {:constraint_violations, _violations}} = result endendTypeScript does not support this workflow yet.Constraint violation assertions
Section titled “Constraint violation assertions”Assert that a specific named constraint fired:
using Cratis.Chronicle.EventSequences;
public static class TestingAssertionsConstraintViolation{ public static void Assert(AppendResult result) { result.ShouldHaveConstraintViolations(); result.ShouldHaveConstraintViolationFor("TestingAssertionsUniqueAuthorName"); }}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.TestingAssertionsConstraintViolationAuthorRegistered do use Chronicle.Events.EventType, id: "testing-assertions-constraint-violation-author-registered"
defstruct [:name]
unique(:name, name: "TestingAssertionsConstraintViolationUniqueAuthorName")end
defmodule MyApp.TestingAssertionsConstraintViolationTest do use ExUnit.Case, async: true
import Chronicle.Testing.Assertions
alias MyApp.Events.TestingAssertionsConstraintViolationAuthorRegistered
test "a duplicate unique name reports the named constraint" do {:ok, client} = Chronicle.Testing.start_supervised_client( event_types: [TestingAssertionsConstraintViolationAuthorRegistered] )
on_exit(fn -> Chronicle.Testing.stop_supervised_client(client) end)
:ok = Chronicle.EventSequences.EventLog.append( "author-#{System.unique_integer([:positive])}", %TestingAssertionsConstraintViolationAuthorRegistered{name: "Jane Smith"}, client: client )
result = Chronicle.EventSequences.EventLog.append( "author-#{System.unique_integer([:positive])}", %TestingAssertionsConstraintViolationAuthorRegistered{name: "Jane Smith"}, client: client )
assert_constraint_violation(result, "TestingAssertionsConstraintViolationUniqueAuthorName") endendimport { field } from '@cratis/fundamentals';import { eventType, unique } from '@cratis/chronicle';import { EventScenario } from '@cratis/chronicle/testing';
class ConstraintEmailRegistered { @field(String) @unique('ConstraintEmail', 'Already used: {PropertyValue}') email: string; constructor(email: string) { this.email = email; }}eventType('ConstraintEmailRegistered')(ConstraintEmailRegistered);
const constraintScenario = new EventScenario({ artifacts: { eventTypes: [ConstraintEmailRegistered] } });await constraintScenario.given.forEventSource('first').events(new ConstraintEmailRegistered('alice'));const constraintResult = await constraintScenario.when.forEventSource('second').event(new ConstraintEmailRegistered('alice'));if (constraintResult.isSuccess || constraintResult.constraintViolations[0]?.message !== 'Already used: alice') { throw new Error('Expected an unscoped unique-value violation');}// No second event was committed; rejected sequence numbers normalize to 0n.Assert that no constraint violations occurred:
using Cratis.Chronicle.EventSequences;
public static class TestingAssertionsNoConstraintViolation{ public static void Assert(AppendResult result) => result.ShouldNotHaveConstraintViolations();}import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.testing.EventScenarioimport kotlinx.coroutines.runBlockingimport org.junit.jupiter.api.Assertions.assertTrueimport org.junit.jupiter.api.Test
@EventTypedata class TestingAssertionsNoViolationAuthorRegistered(val name: String)
/** * [EventScenario] runs in-process with no kernel behind it, so nothing enforces constraints and * every append comes back free of constraint violations - there is no kernel there to reject one. */class TestingAssertionsNoViolationTests {
@Test fun `an in-process append never carries a constraint violation`() = runBlocking { val scenario = EventScenario() val result = scenario.eventLog.append("author-1", TestingAssertionsNoViolationAuthorRegistered("Jane Smith"))
assertTrue(result.constraintViolations.isEmpty()) }}import io.cratis.chronicle.events.EventType;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.assertTrue;
/** * EventScenario runs in-process with no kernel behind it, so nothing enforces constraints and every * append comes back free of constraint violations - there is no kernel there to reject one. */class TestingEventsAssertionsNoConstraintViolation {
@EventType record AuthorRegistered(String name) { }
@Test void anInProcessAppendNeverCarriesAConstraintViolation() { var scenario = new EventScenario("testing", "default"); var eventLog = new BlockingEventSequence(scenario.getEventSequence());
AppendResult result = eventLog.append("author-1", new AuthorRegistered("Jane Smith"));
assertTrue(result.getConstraintViolations().isEmpty()); }}defmodule MyApp.Events.TestingAssertionsNoViolationAuthorRegistered do use Chronicle.Events.EventType, id: "testing-assertions-no-violation-author-registered"
defstruct [:name]end
defmodule MyApp.TestingAssertionsNoConstraintViolationTest do use ExUnit.Case, async: true
alias MyApp.Events.TestingAssertionsNoViolationAuthorRegistered
test "appending two different names does not violate a constraint" do {:ok, client} = Chronicle.Testing.start_supervised_client( event_types: [TestingAssertionsNoViolationAuthorRegistered] )
on_exit(fn -> Chronicle.Testing.stop_supervised_client(client) end)
result = Chronicle.EventSequences.EventLog.append( "author-#{System.unique_integer([:positive])}", %TestingAssertionsNoViolationAuthorRegistered{name: "Jane Smith"}, client: client )
assert result == :ok endendimport { field } from '@cratis/fundamentals';import { eventType, unique } from '@cratis/chronicle';import { EventScenario } from '@cratis/chronicle/testing';
class DistinctEmailRegistered { @field(String) @unique('DistinctEmail') email: string; constructor(email: string) { this.email = email; }}eventType('DistinctEmailRegistered')(DistinctEmailRegistered);
const distinctScenario = new EventScenario({ artifacts: { eventTypes: [DistinctEmailRegistered] } });await distinctScenario.given.forEventSource('first').events(new DistinctEmailRegistered('alice'));const distinctResult = await distinctScenario.when.forEventSource('second').event(new DistinctEmailRegistered('bob'));if (!distinctResult.isSuccess || distinctResult.constraintViolations.length !== 0) { throw new Error('Different string values should be allowed');}Concurrency violation assertions
Section titled “Concurrency violation assertions”using Cratis.Chronicle.EventSequences;
public static class TestingAssertionsConcurrency{ public static void AssertHasViolations(AppendResult result) => result.ShouldHaveConcurrencyViolations();
public static void AssertNoViolations(AppendResult result) => result.ShouldNotHaveConcurrencyViolations();}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Events.TestingAssertionsConcurrencyAccountOpened do use Chronicle.Events.EventType, id: "testing-assertions-concurrency-account-opened"
defstruct [:account_name]end
defmodule MyApp.TestingAssertionsConcurrencyTest do use ExUnit.Case, async: true
import Chronicle.Testing.Assertions
alias Chronicle.Events.ConcurrencyScope alias MyApp.Events.TestingAssertionsConcurrencyAccountOpened
test "a second first-append for the same account is a concurrency violation" do {:ok, client} = Chronicle.Testing.start_supervised_client( event_types: [TestingAssertionsConcurrencyAccountOpened] )
on_exit(fn -> Chronicle.Testing.stop_supervised_client(client) end)
account_id = "account-#{System.unique_integer([:positive])}" scope = ConcurrencyScope.expecting_no_matching_event(event_source_id: true)
:ok = Chronicle.EventSequences.EventLog.append( account_id, %TestingAssertionsConcurrencyAccountOpened{account_name: "Ada"}, client: client, concurrency_scope: scope )
result = Chronicle.EventSequences.EventLog.append( account_id, %TestingAssertionsConcurrencyAccountOpened{account_name: "Ada"}, client: client, concurrency_scope: scope )
assert_concurrency_violation(result) endendTypeScript does not support this workflow yet.Error assertions
Section titled “Error assertions”using Cratis.Chronicle.EventSequences;
public static class TestingAssertionsErrors{ public static void AssertHasErrors(AppendResult result) => result.ShouldHaveErrors();
public static void AssertNoErrors(AppendResult result) => result.ShouldNotHaveErrors();}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.TestingAssertionsErrorsTest do # `{:error, {:append_errors, _}}` is the kernel reporting an append-level # error (as opposed to a constraint or concurrency violation) — for example a # malformed event payload the kernel rejects after the client's own schema # checks pass. `Chronicle.Testing.InMemoryKernel` does not model this failure # class yet, so this documents the match arm without exercising it; use # `Chronicle.Testing.Integration` against a real kernel to reproduce it. use ExUnit.Case, async: true @moduletag :skip
test "append errors are reported as {:error, {:append_errors, errors}}" do result = {:error, {:append_errors, [%{Reason: "malformed-event"}]}}
assert {:error, {:append_errors, errors}} = result assert length(errors) > 0 endendTypeScript does not support this workflow yet.For asserting on the event sequence itself (tail sequence number, event types, and event content), see Event Sequence Assertions.
Example: testing a constraint violation end-to-end
Section titled “Example: testing a constraint violation end-to-end”Pre-seed state with Given, act with When, then assert on the captured scenario.AppendResult — including constraints declared on ConceptAs<T> properties:
using Cratis.Concepts;using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Testing.EventSequences;using Cratis.Specifications;using Xunit;
// Email is a ConceptAs<string> — uniqueness on a domain value works in-process,// so the constraint no longer needs to be hidden behind #if !DEBUG.public record TestingAssertionsEmail(string Value) : ConceptAs<string>(Value);
[EventType]public record TestingAssertionsAuthorRegisteredWithEmail([property: Unique("TestingAssertionsUniqueAuthorEmail")] TestingAssertionsEmail Email);
public class when_registering_an_author_with_a_taken_email : Specification, IDisposable{ readonly EventScenario _scenario = new(); AppendResult _result = default!;
Task Establish() => _scenario.Given .ForEventSource(EventSourceId.New()) .Events(new TestingAssertionsAuthorRegisteredWithEmail(new("john@doe.com")));
async Task Because() => _result = await _scenario.When .ForEventSource(EventSourceId.New()) .Events(new TestingAssertionsAuthorRegisteredWithEmail(new("john@doe.com")));
[Fact] void should_be_rejected() => _result.ShouldBeFailed();
[Fact] void should_report_the_constraint() => _result.ShouldHaveConstraintViolation("TestingAssertionsUniqueAuthorEmail");
public void Dispose() => _scenario.Dispose();}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.Concepts.TestingAssertionsEmail do use Chronicle.Concept, type: :stringend
defmodule MyApp.Events.TestingAssertionsAuthorRegisteredWithEmail do use Chronicle.Events.EventType, id: "testing-assertions-author-registered-with-email"
unique(:email, name: "TestingAssertionsUniqueAuthorEmail")
defstruct email: %MyApp.Concepts.TestingAssertionsEmail{}end
defmodule MyApp.TestingAssertionsConstraintE2eTest do use ExUnit.Case, async: true
import Chronicle.Testing.Assertions
alias MyApp.Concepts.TestingAssertionsEmail alias MyApp.Events.TestingAssertionsAuthorRegisteredWithEmail
test "registering an author with a taken email is rejected" do {:ok, client} = Chronicle.Testing.start_supervised_client( event_types: [TestingAssertionsAuthorRegisteredWithEmail] )
on_exit(fn -> Chronicle.Testing.stop_supervised_client(client) end)
email = %TestingAssertionsEmail{value: "john@doe.com"}
:ok = Chronicle.EventSequences.EventLog.append( "author-#{System.unique_integer([:positive])}", %TestingAssertionsAuthorRegisteredWithEmail{email: email}, client: client )
result = Chronicle.EventSequences.EventLog.append( "author-#{System.unique_integer([:positive])}", %TestingAssertionsAuthorRegisteredWithEmail{email: email}, client: client )
assert_constraint_violation(result, "TestingAssertionsUniqueAuthorEmail") endendimport { field } from '@cratis/fundamentals';import { eventType, unique } from '@cratis/chronicle';import { EventScenario } from '@cratis/chronicle/testing';
class BatchEmailRegistered { @field(String) @unique('BatchEmail') email: string; constructor(email: string) { this.email = email; }}eventType('BatchEmailRegistered')(BatchEmailRegistered);
const batchConstraintScenario = new EventScenario({ artifacts: { eventTypes: [BatchEmailRegistered] } });const batchConstraintResults = await batchConstraintScenario.appendMany([ { eventSourceId: 'first', event: new BatchEmailRegistered('alice') }, { eventSourceId: 'second', event: new BatchEmailRegistered('alice') }]);if (batchConstraintResults.some(result => result.isSuccess) || batchConstraintScenario.appendedEvents.length !== 0) { throw new Error('A conflicting batch must not commit any events');}// Both results carry the batch-level violation; no sequence number was consumed.