Skip to content

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 proveStart withIt does not prove
A calculation or decisionA direct handle() spec with explicit inputsValidation, authorization, provide(), or services
Validation, authorization, provide(), services, and the responseCommandScenarioHTTP routing, authentication handlers, or real infrastructure
Operations executing and compensating in orderCommandScenario with fake providersThat a real provider undid anything
A query’s arguments, paging, sorting, and result shapeQueryScenarioThe database’s own query behavior
A live query’s emissionsObservableQueryScenarioA transport or a browser client
The route, the host, and authenticationArcScenario with HTTP requestsBusiness edge cases you did not send
Events a command appends for Chronicle, from pinned or supported seeded read modelsChronicleCommandScenarioAggregates, unsupported projections, constraints, or observer-driven updates
A command that depends on stored events or an aggregateChronicleKernelScenario against a running kernelReplays 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.

Two lessons build the habit step by step, each with runnable specs:

ScenarioUse it forPage
CommandScenarioA decorated command, its validators, services, authorization, and operationsCommands
QueryScenarioA decorated static query, with arguments, paging, and sortingQueries
ObservableQueryScenarioA decorated observable query, collecting emissions with a deadlineObservable queries
ArcScenarioLow-level definitions and full HTTP requestsLow-level definitions
ChronicleCommandScenarioCommands that return Chronicle events, without a kernelChronicle
ChronicleKernelScenarioChronicle commands with seeded events, aggregates, projections, and constraintsChronicle 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.

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:

Features/Tasks/Registration/for_RegisterTask/given/a_task_registration.ts
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.

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.

Start with Test a command’s decision and its pipeline, or go straight to Testing commands for every assertion.