Test a behaviour
A test proves one behaviour. Name that behaviour before you write the test. Then pick the shape below that fits it.ΒΆ
Each shape returns a fixed, independent answer. A test that reruns the code under test cannot catch its mistakes. Read tests that earn their place22 first.ΒΆ
1. A domain rule: a property
A rule that holds for many inputs is an it.prop over Effect Arbitrary inputs. Use it.effect.prop when the check returns an Effect.ΒΆ
The events suite generates a sensitive key and requires its removal. The assertion does not call the filter to decide the answer:ΒΆ
it.prop(
"credentials, emails, codes, and session ids in a query never reach an event",
{
pairs: queryPairs,
secret: sensitiveWord,
value: word,
},
({ pairs, secret, value }) => {
const captured = capturedQuery(
new URLSearchParams([...pairs, [secret, value]])
);
expect(Object.hasOwn(captured, secret)).toBe(false);
}
);Read property-based testing18.ΒΆ
2. Anything with history: a model
Sessions, retries, and lifecycles depend on earlier steps. Generate command sequences and check the real system against a small model after each step.ΒΆ
The inspection lifecycle replays generated histories against the real actor:ΒΆ
it.effect.prop(
"real actor preserves generated input, typed outcomes and terminal immutability",
{ generated: commands, scenario: Scenario },
({ generated, scenario }) => replayActor(scenario, generated),
{ arbitrary: { runs: 200 } }
);Copy skills/add-a-lifecycle-machine/templates/lifecycle.model.test.md for a new machine. Read model-based testing14.ΒΆ
3. UI behaviour: a Foldkit Scene
A Scene runs update, the view, and subscriptions together. Input and time are data:ΒΆ
- Input is a step:
pointerDown,click,type, orSubscription.emit.ΒΆ - Time is a Command. Resolve it with
Command.resolve(WaitX, CompletedWaitX(...)).ΒΆ - Do not use fake timers, a clock, or a DOM.ΒΆ
A race is a plain assertion. Resolve a stale timer after the state has moved on, then assert that nothing broke.ΒΆ
Assert on what the view shows: roles, text, and data attributes. Do not assert on Model fields.ΒΆ
The readerβs copy button resolves the copy and then its reset wait:ΒΆ
it("a copy confirms with text the reader can see and hear, then resets after the wait", () => {
scene(
{ update, view },
given(clipboardReady),
click(copyButton),
Command.expectExact(CopyReaderText({ id, text: promptText })),
Command.resolve(CopyReaderText, Message.SucceededCopyReaderText({ id })),
expect(role("status")).toHaveText("Copied β"),
expect(copyButton).toContainText("Copied β"),
Command.expectExact(WaitBeforeCopyReset({ id })),
Command.resolve(
WaitBeforeCopyReset,
Message.CompletedWaitBeforeCopyReset({ id })
),
expect(role("status")).toHaveText(""),
expect(copyButton).toContainText(label),
Command.expectNone()
);
});Read UI behaviour is a scene7 for the Foldkit16 UI Toast race.ΒΆ
4. A seam: one example
An example test fits one interface between parts, such as a service over the real filesystem. Keep one, and supply services through a Layer:ΒΆ
it.layer(TestLayer)("FileInspector", (test) => {
test.effect("reads a file through the Effect filesystem", () =>
Effect.gen(function* readsThroughFileSystem() {
const file = yield* writeTempFile("notes.txt", "one two\nthree\n");
const inspector = yield* FileInspector;
const stats = yield* inspector.inspect(file);
expect(stats).toMatchObject({ bytes: 14, lines: 2, words: 3 });
})
);5. Always: plant a bug
- Break the behaviour the test protects.ΒΆ
- Run the test. It must fail.ΒΆ
- Restore the code. The test must pass.ΒΆ
Reject these tests:ΒΆ
- A tautological test computes its expected answer with the code under test.ΒΆ
- A change-detector test fails on any edit and proves no rule.ΒΆ
- A regression test for a bug the suite already covers adds nothing.ΒΆ
Do not rewrite an expected result to match broken code.ΒΆ
Last changed 2026-10-09 in 3107109. Source on GitHub. Change log.