Skip to content

Fixture tests an aggregate in isolation: given these events, when this command runs, what happens? It runs commands through the real service on an in-memory store, so payload validation and event encoding are tested too.

It reads like the CQRSFixture of moleculer-cqrs. The chain is an Effect, which runs when you yield it or return it from it.effect:

import { assert, it } from "@effect/vitest"
import { Fixture } from "effect-cqrs"
it.effect("creates a todo", () =>
Fixture.make(Todo)
.givenEvents()
.when("createTodo", { title: "Buy Milk" })
.expectEvent(TodoEvent.cases.TodoCreated.make({ title: "Buy Milk", createdAt: DateTime.makeUnsafe(0) }))
.inspectState((state) => assert.strictEqual(state.title, "Buy Milk")))
it.effect("runs commands one after another", () =>
Fixture.make(Todo)
.givenEvents([created])
.when("deleteTodo", {})
.expectEvent("TodoDeleted")
.when("createTodo", { title: "Buy Eggs" })
.expectEvents(["TodoCreated"]))
it.effect("rejects deleting twice", () =>
Effect.gen(function*() {
const error = yield* Fixture.make(Todo)
.givenEvents([created, deleted])
.whenFails("deleteTodo", {})
assert.strictEqual(error._tag, "TodoAlreadyDeleted")
}))
Step What it does
givenEvents(events?, aggregateId?) The history the commands run against, empty by default. It can include old events
when(command, payload) Runs a command, typed against the aggregate. A command that fails stops the chain with its error
expectEvent(event) The last command emitted exactly this event
expectEvents([...]) The last command emitted exactly these events, in order. [] means it recorded nothing
inspectState(check) Passes the state to a callback that throws to fail the test, so any assertion library works
whenFails(command, payload) Runs a command that should fail, and returns its error. It dies if the command succeeds

An expected event is either a whole event, compared with the aggregate’s schema, or only its tag. A mismatch dies with ExpectationFailed, which carries what was expected and what happened, and Vitest shows the difference.

Yielding the chain gives { events, state, version, fixture } for the last command, if you’d rather write plain assertions.

givenEvents also takes events in an older version of their schema, built with Aggregate.historical. They are stored as code from that time would have stored them, so the history goes through the upcasting a real store does:

it.effect("upcasts todos created before due dates", () =>
Fixture.make(Todo)
.givenEvents([Aggregate.historical(Todo, "TodoCreated", 1, { title: "Buy Milk", createdAt })])
.inspectState((state) => assert.strictEqual(state.dueDate, null)))

The value is checked against that version’s schema, at compile time and when it runs, so a test can’t seed a shape that never existed. For JSON taken from a database, new Aggregate.Historical({ type, schemaVersion, payload }) skips the check.

checkInvariants runs random sequences of commands, with payloads generated from their schemas by Effect’s Arbitrary, and checks a predicate on the state after every command that succeeds. Commands that fail are part of the game. A violation dies with ExpectationFailed, holding the shortest sequence it found that breaks the predicate.

it.effect("never overdraws", () =>
Fixture.make(Account)
.givenEvents([opened])
.checkInvariants({
invariant: (state) => state.balance >= 0,
commands: {
closeAccount: false, // left out
deposit: Arbitrary.schema(SmallAmount).pipe(Arbitrary.map((amount) => ({ amount })))
},
maxCommands: 20, // the longest sequence, 20 by default
check: { runs: 200, seed: 1 } // passed to Arbitrary.checkEffect
}))

Each run starts from the given events on a fresh store.

it.effect runs on a test clock stopped at zero, so timestamps need no mocking: an event created with DateTime.now has DateTime.makeUnsafe(0) until TestClock.adjust moves time.