Skip to content
Agentmart
Submit a tool

effect-testing

Testing Effect code with @effect/vitest, TestClock and test layers.

Skill by andabove

IndexedMIT
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/andabove-effect-testing

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Testing Effect code with @effect/vitest, TestClock and test layers. Use when you write or review tests for Effect programs or services, control time in a test, replace a service with a test layer, or test an Effect module through its plain async function.

Use cases: testing, writing

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/effect-testing

What's inside

Skill name
effect-testing
Description
Testing Effect code with @effect/vitest, TestClock and test layers. Use when you write or review tests for Effect programs or services, control time in a test, replace a service with a test layer, or test an Effect module through its plain async function.
License
MIT
Instructions
152 lines in SKILL.md · view on GitHub

Files (2)

  • SKILL.md
  • effect-testing-vitest-api.md · references/

SKILL.md (first lines)

# Testing Effect code

Checked against `effect 4.0.0` and `@effect/vitest 4.0.0`, which needs `vitest` 5 (`>=5.0.0 <6.0.0`). When the project has newer versions, read `node_modules/@effect/vitest/src/index.ts`, `node_modules/effect/src/testing/TestClock.ts` and `node_modules/effect/AGENTS.md` before you trust a name here.

## Pick the boundary

- **A module that exports a plain async function** with an Effect program inside (see `skill:effect-adoption`): test that function with ordinary Vitest tests. The tests hold no Effect type. See [Test through the plain async edge](#test-through-the-plain-async-edge).
- **Effect programs and services** that other Effect code calls: test them with `@effect/vitest`, where each test returns an Effect.

## The helpers in @effect/vitest 4.0.0

| Helper | What the test gets |
|---|---|
| `it.effect(name, () => effect, timeout?)` | A `TestClock` at time 0, a `TestConsole`, and a fresh `Scope` that closes after the test. |
| `it.live(name, () => effect, timeout?)` | The real clock and console, and a fresh `Scope`. |
| `layer(L)(name, (it) => { ... })`, `it.layer(L)` | One build of `L` shared by every test in the block, plus the test services. |
| `it.effect.each(cases)(name, (testCase) => effect)` | One test for each case. |
| `it.effect.prop(name, arbitraries, (values) => effect)`, `it.prop` | A property test with inputs from `Schema` or `Arbitrary` values. |
| `it.flakyTest(effect, timeout?)` | The effect, run again on failure up to 10 times within `timeout` (30 seconds by default). |
| `makeMethods(test.extend(...))` | The same helpers, for a Vitest test with fixtures. |

`it.effect` and `it.live` also have `.skip`, `.only`, `.fails`, `.skipIf(condition)` and `.runIf(condition)`. Import `it`, `layer`, `assert`, `expect` and `describe` from `@effect/vitest`: it re-exports the Vitest API.

Write `it.effect`, not `it.scoped`. Version 4.0.0 has no `it.scoped` and no `it.scopedLive`, because `it.effect` and `it.live` already give each test a scope. `it.scoped` still resolves, to Vitest's deprecated fixture API: the test is not registered and its body never runs. A file with other tests passes; a file with only such tests fails with "No test suite found". Vitest does not type check, so only `tsc` catches it (`Expected 1 arguments, but got 2`). Run `tsc` over the test files.

Signatures and options for each helper are in [references/effect-testing-vitest-api.md](references/effect-testing-vitest-api.md).

## Write an it.effect test

```ts
import { assert, describe, it } from "@effect/vitest"
import * as Context from "effect/Context"
import * as Data from "effect/Data"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"

class OutOfStock extends Data.TaggedError("OutOfStock")<{ readonly sku: string }> {}

class Stock extends Context.Service<Stock, {
  reserve(sku: string): Effect.Effect<number, OutOfStock>
}>()("app/Stock") {}

// The state is made inside the build, so each Effect.provide starts with one item.
const StockTest = Layer.effect(Stock, Effect.sync(() => {
  let left = 1
  return Stock.of({
    reserve: (sku) => Effect.suspend(() => left === 0 ? Effect.fail(new OutOfStock({ sku })) : Effect.succeed(--left))
  })
}))

describe("Stock", () => {
  it.effect("reserves the last item", () =>
    Effect.gen(function*() {
      const stock = yield* Stock
      assert.strictEqual(yield* stock.reserve("a1"), 0)
    }).pipe(Effect.provide(StockTest)))

  it.effect("fails with OutOfStock when no item is left", () =>
    Effect.gen(function*() {
      const stock = yield* Stock
      yield* stock.reserve("a1")
      const error = yield* Effect.flip(stock.reserve("a1"))
      assert.strictEqual(error._tag, "OutOfStock")
      assert.strictEqual(error.sku, "a1")
    }).pipe(Effect.provide(StockTest)))
})
```

- **Assert inside the Effect.** The test fails when the returned Effect fails, dies or throws.
- **Get an expected error with `Effect.flip`, and assert its `_tag` and fields.** `Effect.flip` turns the error into the success value.
- **Do not compare a whole failed `Exit`** with `assert.deepStrictEqual(exit, Exit.fail(error))` or `assertExitFailure`. Both compare the `Cause` and its annotations. When the code under test uses `Effect.fn`, the failure carries a stack trace annotation, and the comparison fails though the error is equal. Compare a whole `Exit` only in a test that controls the annotations.
- **Use the `@effect/vitest/utils` helpers** for values: `assertExitSuccess`, `assertSome`, `assertNone`, `assertSuccess`, `assertFailure` (for `Result`).
- **Read logs from `TestConsole`.** In `it.effect`, `Console.log` and `Effect.log` write to the `TestConsole`, not to the terminal. Read them with `yield* TestConsole.logLines` (or `errorLines`) from `effect/testing/TestConsole`. `it.live` prints them.
- **Expect cleanup on timeout.** A Vitest timeout interrupts the test fiber, and its finalizers run.

## Control time with TestClock

Under `it.effect`, time stands still until the test moves it. Fork the code that waits, move the clock, then join. Check both sides of a deadline: the fiber is still running just before it, and done at it. A test that only moves past the deadline also passes when the code times out too early.

```ts
import { assert, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import * as Option from "effect/Option"
import * as TestClock from "effect/testing/TestClock"

it.effect("times out a slow lookup at 5 seconds", () =>
  Effect.gen(function*() {
    const lookup = Effect.sleep("1 minute").pipe(Effect.as("found"))
    const fiber = yield* lookup.pipe(Effect.timeoutOption("5 seconds"), Effect.forkChild)
    yield* TestClock.adjust("4999 millis")
    assert.isUndefined(fiber.pollUnsafe())
    yield* TestClock.adjust("1 millis")
    assert.deepStrictEqual(yield* Fiber.join(fiber), Option.none())
  }))
```

- **Move the clock for every sleep.** A sleep that no test adjusts waits until the Vitest timeout. The TestClock warning is a log line, and `it.effect` sends it to the `TestConsole`, so you see only the timeout.
- **Make fakes settle at once.** `TestClock.adjust` yields between sleeps and checks again for sleeps that are due, so one adjust also drives a sleep that a woken fiber registers during the adjust: one 30 ms adjust runs retries at 0, 10, 20 and 30 ms. It cannot drive a sleep that is registered after the adjust has finished. That happens when a fake answers in a later macrotask (`setTimeout`, real I/O): the adjust ends while the call is pending, the retry delay starts later, and the fiber waits. A fake that returns `Promise.reject(...)`, `Promise.resolve(...)` or an Effect settles during the adjust.
- **Use `TestClock` only where the test clock is provided.** `it.effect`, `layer(...)` and `Effect.provide(TestClock.layer())` provide it. Under `it.live` or a plain `Effect.runPromise`, `TestClock.adjust` dies with `testClock.adjust is not a function`.
- **Read time through `Clock` or `DateTime.now`.** They follow the test clock, and `TestClock.setTime(Date.UTC(...))` sets a date. `Date.now()` and `new Date()` read the real time.
- **Run one step on the real clock** with `TestClock.withLive(effect)`.

## Replace a service with a test layer

- **Build a stateless fake with `Layer.succeed(Service, Service.of({ ... }))`.** For a partial fake, use `Layer.mock(Service, { ... })`: an omitted Effect member dies with `UnimplementedError` naming the method when a test calls it.
- **Make a stateful fake's state inside the build,** with `Layer.effect(Service, Effect.sync(() => { let state = ...; return Service.of({ ... }) }))`, as in the example above. Then `Effect.provide(TestLayer)` on each test's Effect gives each test fresh state. A fresh build does not reset state that the layer captured from outside: with `Layer.succeed` over a module-level variable, the second test sees what the first test left.
- **Share a layer across a block** with `layer(TestLayer)("name", (it) => { ... })` only for a resource that is costly to build or that no test changes. The block builds the layer once; a test sees the state that the tests before it left.
- **Expose the fake's state to the test** with `Layer.provideMerge`: build the state as its own service, and merge it into the fake's layer so the test can `yield*` it and assert on it.
- `layer(...)` adds `TestClock` and `TestConsole` unless you pass `{ excludeTestServices: true }`.

For services, layers and `Layer.provideMerge`, see `skill:effect-services`.

## Pin public signatures with a type test

A runtime test cannot see a lost case in an error type. The code that raised it is gone, and a declared return type still compiles (see `skill:effect-errors`). Pin each public Effect signature in a `*.test-d.ts` file with Vitest's `expectTypeOf`:

```ts nocheck
// getItem.test-d.ts: import the function under test, never redeclare its type here.

From andabove/skills, MIT.

More from andabove/skills

Signals

Stars
0
Forks
0
Open issues
0
Last commit (this folder)
Oct 3, 2026 (26 days ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from andabove

Cite this page

effect-testing skill. Agent Mart. https://agentmart.dev/skills/andabove-effect-testing (last updated 2026-10-24)