Testing
A spec that calls handle() directly skips everything Arc does around it: binding, authorization, validators, services, and the result envelope. A spec that starts an HTTP server is slow and fragile. @cratis/arc.testing runs your artifacts through the real pipelines in-process, so a passing spec means the behavior a client sees, without a listener or a port.
A useful spec still answers one precise question. Is the decision right? Did Arc enforce the rule? Did the operation get undone? Each question has a boundary that answers it cheaply.
Choose the boundary that can catch the bug
Section titled “Choose the boundary that can catch the bug”| What you need to prove | Start with | It does not prove |
|---|---|---|
| A calculation or decision | A direct handle() spec with explicit inputs | Validation, authorization, provide(), or services |
Validation, authorization, provide(), services, and the response | CommandScenario | HTTP routing, authentication handlers, or real infrastructure |
| Operations executing and compensating in order | CommandScenario with fake providers | That a real provider undid anything |
| A query’s arguments, paging, sorting, and result shape | QueryScenario | The database’s own query behavior |
| A live query’s emissions | ObservableQueryScenario | A transport or a browser client |
| The route, the host, and authentication | ArcScenario with HTTP requests | Business edge cases you did not send |
| Events a command appends for Chronicle, from pinned or supported seeded read models | ChronicleCommandScenario | Aggregates, unsupported projections, constraints, or observer-driven updates |
| A command that depends on stored events or an aggregate | ChronicleKernelScenario against a running kernel | Replays or concurrent writers after the check |
Combine boundaries rather than pushing every case through the widest one. Cover decision branches with fast direct specs, add scenario specs for the Arc contracts that matter, and keep a smaller set of HTTP or integration tests for real composition.
Learn by doing
Section titled “Learn by doing”Two lessons build the habit step by step, each with runnable specs:
- Test a command’s decision and its pipeline tests one shipping-quote command both ways: directly for the arithmetic, through
CommandScenariofor validation and the service call. - Test operations and compensation proves that Arc runs a command’s operations, stops after a provider failure, and compensates in reverse order.
Pick a scenario
Section titled “Pick a scenario”| Scenario | Use it for | Page |
|---|---|---|
CommandScenario | A decorated command, its validators, services, authorization, and operations | Commands |
QueryScenario | A decorated static query, with arguments, paging, and sorting | Queries |
ObservableQueryScenario | A decorated observable query, collecting emissions with a deadline | Observable queries |
ArcScenario | Low-level definitions and full HTTP requests | Low-level definitions |
ChronicleCommandScenario | Commands that return Chronicle events, without a kernel | Chronicle |
ChronicleKernelScenario | Chronicle commands with seeded events, aggregates, projections, and constraints | Chronicle kernel scenarios |
Every scenario builds its application lazily on the first call, lets you register fakes first, and must be disposed with await scenario.dispose(); disposal is idempotent. For the built-in Node.js runner, run a CommandScenario with node:test using after for disposal.
Share a context with given()
Section titled “Share a context with given()”The Tasks sample’s specs use given(Context, context => { ... }) from @cratis/arc.testing to create one context per spec suite, without depending on Mocha types:
import { CommandScenario } from '@cratis/arc.testing';import { Tasks } from '../../../Tasks.js';import { RegisterTask, RegisterTaskValidator } from '../../Registration.js';import { metadata } from '../../../../generatedMetadata.js';
export class a_task_registration { tasks = new Tasks(); scenario = CommandScenario.for(RegisterTask, RegisterTaskValidator);
constructor() { this.scenario.extend(builder => builder.useGeneratedMetadata(metadata)); this.scenario.services.addSingleton(Tasks, this.tasks); }}extend(...) installs anything the application’s builder needs before the scenario builds it; here, the generated metadata that binds handle(tasks: Tasks) without @inject. Because given(...) creates one context for the whole describe, run the action in beforeAll and dispose in afterAll. A scenario disposed after the first test cannot run again.
The for_<Subject>/when_<action>/<case>.ts layout follows the Cratis specification conventions; each spec file reads as a sentence. Run the sample’s specs with yarn vitest run Samples/Tasks.
Wire round trips
Section titled “Wire round trips”Scenario inputs are always encoded to Arc’s wire representation before the pipeline decodes them, exactly as over HTTP. By default inputs and returned data also pass through JSON stringify and parse, so a concept in a response arrives as its primitive value. withSerializationRoundTrip(false) skips only that JSON step and keeps wire encoding.
Next step
Section titled “Next step”Start with Test a command’s decision and its pipeline, or go straight to Testing commands for every assertion.
Related
Section titled “Related”- Calling commands from code, the entry points scenarios build on
- Dependency injection