Chronicle.Testing (cratis_chronicle v3.16.1)

Copy Markdown View Source

Testing support for applications built on Chronicle: an in-memory fake kernel, deterministic helpers, and assertions — so a spec can exercise commands, reactors, reducers, constraints and concurrency without a running kernel.

Usage

{:ok, client} =
  Chronicle.Testing.start_supervised_client(
    event_types: [MyApp.Events.AccountOpened],
    reducers: [MyApp.Reducers.AccountReducer]
  )

:ok = Chronicle.EventSequences.EventLog.append("account-1", %AccountOpened{}, client: client)
:ok = Chronicle.Testing.wait_for_observers(client)

{:ok, %MyApp.ReadModels.Account{} = account} =
  Chronicle.Testing.read_model(client, MyApp.Reducers.AccountReducer, "account-1")

Chronicle.Testing.stop_supervised_client(client)

Prefer Chronicle.Testing.Case in ExUnit tests, which does this setup/teardown for you and is safe under async: true.

What runs for real, and what is modeled

start_supervised_client/1 starts a real Chronicle.Client pointed at a Chronicle.Testing.InMemoryKernel instead of a socket — so command validation, Chronicle.EventSequences.EventLog's append/read/unit-of-work code, constraint declarations, and concurrency scopes all run completely unchanged. Reactors and reducers do not run through the real gRPC observation stream (the in-memory kernel does not simulate bidirectional streaming): they run synchronously, in a separate Chronicle.Testing.Dispatcher process, right after each committed append, by calling the exact same Chronicle.Reactors.Dispatch and Chronicle.Reducers.Dispatch modules the real stream-handling processes use.

Reducer read model state is kept by the dispatcher, keyed by event source id — read_model/3 reads it directly; there is no in-memory ReadModels/Projections query surface. Projection read models (declared with from/join/removed_with, not a reducer) are evaluated by Chronicle.Testing.Projections for the supported subset.

closes_stream constraints and any RPC outside the list in Chronicle.Testing.InMemoryKernel's moduledoc return {:error, {:not_supported_in_memory, rpc}} — reach for Chronicle.Testing.Integration and a real kernel for those.

Summary

Functions

Returns every event appended for event_source_id, decoded into Chronicle.Events.AppendedEvent structs, in append order.

Reads the read model state a reducer has built for key (typically an event source id). Call wait_for_observers/1 first if the append that produced it might not have been processed yet.

Starts a Chronicle.Client supervision tree against an in-memory kernel, and a dispatcher that runs the given reactors and reducers synchronously after every committed append.

Stops a client started by start_supervised_client/1: the dispatcher, the Chronicle.Client supervision tree, and the in-memory kernel, in that order.

Waits for every reactor and reducer the client's dispatcher owns to finish processing every append committed so far. Deterministic — no sleeps: it sends a synchronous call to the dispatcher, which can only answer once its mailbox (where the kernel's append notifications arrive, in order) has drained up to this point.

Functions

appended_events(client, event_source_id)

@spec appended_events(atom(), String.t()) :: [Chronicle.Events.AppendedEvent.t()]

Returns every event appended for event_source_id, decoded into Chronicle.Events.AppendedEvent structs, in append order.

read_model(client, reducer_module, key)

@spec read_model(atom(), module(), String.t()) :: {:ok, struct() | nil}

Reads the read model state a reducer has built for key (typically an event source id). Call wait_for_observers/1 first if the append that produced it might not have been processed yet.

start_supervised_client(opts \\ [])

@spec start_supervised_client(keyword()) :: {:ok, atom()} | {:error, term()}

Starts a Chronicle.Client supervision tree against an in-memory kernel, and a dispatcher that runs the given reactors and reducers synchronously after every committed append.

Options

  • :name — the client name other calls address it by (default: a unique generated atom)
  • :event_store / :namespace — default to a unique generated name / "Default"
  • :event_types — event type modules to register
  • :event_sources — Chronicle.EventSources.EventSource modules to register
  • :constraints — standalone constraint modules (each use Chronicle.Events.Constraint); combined with constraints declared on :event_types
  • :reactors — reactor modules run synchronously by the dispatcher
  • :reducers — reducer modules run synchronously by the dispatcher
  • :projections — read model modules evaluated by Chronicle.Testing.Projections instead of a reducer (optional; this testing kernel does not register or run the server-side projection engine)

Returns {:ok, client}, where client is the name to pass as :client to Chronicle.EventSequences.EventLog, Chronicle.append/3, etc.

stop_supervised_client(client)

@spec stop_supervised_client(atom()) :: :ok

Stops a client started by start_supervised_client/1: the dispatcher, the Chronicle.Client supervision tree, and the in-memory kernel, in that order.

wait_for_observers(client)

@spec wait_for_observers(atom()) :: :ok | {:error, [term()]}

Waits for every reactor and reducer the client's dispatcher owns to finish processing every append committed so far. Deterministic — no sleeps: it sends a synchronous call to the dispatcher, which can only answer once its mailbox (where the kernel's append notifications arrive, in order) has drained up to this point.

Returns :ok, or {:error, failures} when a reactor or reducer raised while processing an event (each failure is {:reactor | :reducer, module, reason}).