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:ΒΆ

packages/events/test/request-facts.test.ts Β· 1d4d3252
  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:ΒΆ

packages/core/test/inspect-machine.model.test.ts Β· 1d4d3252
  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:ΒΆ

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:ΒΆ

apps/web/test/reader.scene.test.ts Β· 1d4d3252
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:ΒΆ

packages/core/test/file-inspector.test.ts Β· 1d4d3252
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

  1. Break the behaviour the test protects.ΒΆ
  2. Run the test. It must fail.ΒΆ
  3. Restore the code. The test must pass.ΒΆ

Reject these tests:ΒΆ

Do not rewrite an expected result to match broken code.ΒΆ


Last changed 2026-10-09 in 3107109. Source on GitHub. Change log.