---
title: Testing
description: Test services as plain Effect programs, render RECs with renderRec, and drive virtual time with TestClock — no DOM ceremony, no real timers.
group: Core
order: 9
---

effract is built so the parts worth testing are testable **without** React. State and logic live in
**services** — plain Effect programs — so most of your tests never mount a component at all. When you do
need to render, `@tmonier/effract-test` handles the `act` / Suspense / timing ceremony for you.

```bash
npm i -D @tmonier/effract-test
```

## Test a service as a plain Effect program

A service is an Effect program with no React in sight, so you test it the way you test any Effect: build
its layer, run the program, assert. No `act`, no DOM, no renderer.

```ts
import { it } from '@effect/vitest';
import * as Effect from 'effect/Effect';
import { expect } from 'vitest';

it.effect('inc bumps the count atom', () =>
  Effect.gen(function* () {
    const counter = yield* Counter;
    expect(counter.count.value).toBe(0);
    counter.inc();
    expect(counter.count.value).toBe(1);
  }).pipe(Effect.provide(Counter.layer)),
);
```

This is the point of keeping logic in services: the bulk of your test suite is fast, deterministic, and
renderer-agnostic. A REC is then a thin view over already-tested logic.

## Drive atoms directly — `readAtom` / `setAtom` / `collectAtom`

To arrange or observe reactive state without a component, reach for the atom helpers. `collectAtom`
subscribes and records **every** value an atom takes — the clean way to assert the _sequence_ a mutation
or a stream produces, not just the final value.

```ts
import { readAtom, setAtom, collectAtom } from '@tmonier/effract-test';

const seen = collectAtom(counter.count); // [0]
counter.inc();
counter.inc();
expect(seen.values).toEqual([0, 1, 2]);
seen.stop();

setAtom(counter.count, 10); // arrange state directly
expect(readAtom(counter.count)).toBe(10);
```

## Render a REC — `renderRec`

`renderRec(node)` mounts a `mount(layer, Root)` tree with the `act`/Suspense plumbing handled and returns
helpers to drive and inspect it. Pass `{ strict: true }` to wrap the tree in `<StrictMode>` and shake out
unsafe effects (effract's disposal is StrictMode-safe by design — this proves it).

```tsx
import { renderRec } from '@tmonier/effract-test';
import { mount } from '@tmonier/effract';

const r = await renderRec(mount(AppLive, Dashboard.suspense(<span>loading…</span>)));

expect(r.text()).toContain('loading…'); // the suspense fallback, before data settles
await r.flush();                         // settle the resolved Suspense
expect(r.text()).toContain('Ada');

await r.click('[data-action="save"]');   // events fire inside act
expect(r.find('[role="status"]')?.textContent).toBe('Saved');

await r.unmount();                        // interrupts fibers, runs finalizers
```

The result gives you `container`, `flush()` (settle microtasks + React work), `settle(ms)` (advance real
time inside `act`, for real-timer effects), `text()`, `find(selector)`, `click(selector)`, and
`unmount()`.

## Virtual time — `TestClock`

Retries, `staleTime`, debounces — anything time-dependent is tested with Effect's **`TestClock`**, so a
"30-second" staleness window elapses instantly and deterministically, with no real timers. Because the
logic lives in a service, you advance the clock and assert on the service, no rendering required.

```ts
import { it } from '@effect/vitest';
import * as Effect from 'effect/Effect';
import * as TestClock from 'effect/TestClock';
import { expect } from 'vitest';

it.effect('revalidates once the staleTime window passes', () =>
  Effect.gen(function* () {
    const prices = yield* Prices;
    expect(yield* prices.fetches).toBe(1);

    yield* TestClock.adjust('30 seconds'); // no real wait
    yield* prices.read();                  // now stale → revalidates
    expect(yield* prices.fetches).toBe(2);
  }).pipe(Effect.provide(Prices.layer)),
);
```

## What to test where

| You want to assert… | Test it as… |
| --- | --- |
| business logic, state transitions, derived values | a **service** — plain `it.effect` + its layer |
| retry / staleness / debounce / any timing | a **service** + **`TestClock.adjust`** |
| the exact sequence a mutation or stream emits | **`collectAtom`** on the atom |
| that the tree renders, suspends, and handles events | **`renderRec`** on `mount(layer, Root)` |
| StrictMode-safe disposal (no leaked fibers/effects) | **`renderRec(node, { strict: true })`** |

effract itself is tested this way — the interpreter with fakes and no DOM, services as Effect programs,
and `renderRec` for the render-path behaviour. Keep your tests behavioural and service-first; reach for
`renderRec` only when the assertion is genuinely about what React put on screen.
