Testing
Effect programs are values that describe work, so testing them is mostly a
matter of running those values and asserting on the result. The @effect/vitest
package wires Effect into Vitest so a test can simply
return an Effect, and the surrounding machinery runs it, fails the test on an
unexpected error, and tears down any scoped resources for you.
Because Effect models time, services, and randomness as data rather than calling
into the platform directly, tests can be fully deterministic. Sleeps,
timeouts, retries, and schedules are driven by a controllable TestClock
instead of real wall-clock time, and the services your code depends on are
swapped for in-memory test implementations through Layer.
import { assert, describe, it } from "@effect/vitest"import { Effect } from "effect"
// A test is just an Effect. `it.effect` runs it and provides the test// services (TestClock, TestConsole) automatically.describe("greeting", () => { it.effect("uppercases the name", () => Effect.gen(function*() { const result = "ada".toUpperCase() assert.strictEqual(result, "ADA") }))})Why tests are deterministic
Section titled “Why tests are deterministic”An Effect never does anything by itself — it is a description that the Effect
runtime interprets. That description reaches for time, the console, randomness,
and your application services through the context, so a
test can replace any of them. it.effect does exactly this: it runs your Effect
against a runtime where two services are swapped for test-friendly versions.
TestClock— time does not advance on its own. The clock starts at the epoch (0) and only moves when you callTestClock.adjust. A program that sleeps for an hour completes instantly once you advance the clock by an hour, with no real waiting. See TestClock.TestConsole—Console.log/Console.errorcalls are captured in memory instead of being printed, so you can assert on what a program logged.
Everything else (the order of fibers, retries, schedules) is driven by these deterministic services, so the same test produces the same result every run.
@effect/vitest builds on Vitest. Add both as dev dependencies:
pnpm add -D vitest @effect/vitestThen import the enhanced it (and assert, describe, expect, layer,
prop) from @effect/vitest instead of from vitest directly. The package
re-exports everything from vitest, so this is your single entry point:
import { assert, describe, expect, it, layer, prop } from "@effect/vitest"This it is the standard Vitest test function extended with Effect-aware
methods. The most important ones:
| Method | Description |
|---|---|
it.effect | Run an Effect with the test services provided (TestClock, TestConsole). |
it.live | Run an Effect against the live runtime — real clock, real console. |
it.effect.each | Run the same Effect test over a table of cases. |
it.effect.prop | Property-based testing: generate inputs from Schema arbitraries. |
it.flakyTest | Retry an Effect until it succeeds (or a timeout elapses). |
layer(...) | Build a layer once and share its services across every test in a block. |
Assertions come from the same import: the assert namespace re-exports
Vitest’s assert (assert.strictEqual, assert.deepStrictEqual,
assert.isTrue, …). Effect-aware helpers for Option, Result, and Exit
(assertSome, assertSuccess, assertExitSuccess, …) live in
@effect/vitest/utils (see the assertions reference
below).
The grab bag
Section titled “The grab bag”The @effect/vitest patterns you reach for most. Each has a fuller treatment on
the sub-pages linked at the bottom; this is the shortlist.
Run an Effect as a test, capture failures
Section titled “Run an Effect as a test, capture failures”import { assert, it } from "@effect/vitest"import { Effect, Exit } from "effect"
// `it.effect` provides TestClock + TestConsole and a Scope, runs the Effect,// and fails the test if it fails unexpectedly.it.effect("adds", () => Effect.gen(function*() { assert.strictEqual(yield* Effect.succeed(1 + 1), 2) }))
// A failing Effect would abort the test — capture the outcome with Effect.exit// (or Effect.result) and assert on the value instead.it.effect("fails as expected", () => Effect.gen(function*() { const exit = yield* Effect.exit(Effect.fail("DivByZero")) assert.deepStrictEqual(exit, Exit.fail("DivByZero")) }))Run against the live runtime with it.live
Section titled “Run against the live runtime with it.live”import { assert, it } from "@effect/vitest"import { Clock, Effect } from "effect"
// Real Clock, real Console, real randomness — use it only when you must observe// actual timing. A program that sleeps will actually wait.it.live("reads real time", () => Effect.gen(function*() { const millis = yield* Clock.currentTimeMillis assert.isTrue(millis > 0) // => real epoch millis, not 0 }))Drive virtual time with TestClock
Section titled “Drive virtual time with TestClock”import { assert, it } from "@effect/vitest"import { Effect, Fiber } from "effect"import { TestClock } from "effect/testing"
// Fork the time-dependent work, advance the clock, then join and assert.it.effect("completes a one-hour sleep instantly", () => Effect.gen(function*() { const fiber = yield* Effect.forkChild( Effect.sleep("1 hour").pipe(Effect.as("done" as const)) ) yield* TestClock.adjust("1 hour") assert.strictEqual(yield* Fiber.join(fiber), "done") }))Table-driven tests with each
Section titled “Table-driven tests with each”import { assert, it } from "@effect/vitest"import { Effect } from "effect"
// One test per row; `%#` in the name is replaced with the case index.it.effect.each([ { input: "ada", expected: "ADA" }, { input: "grace", expected: "GRACE" }])("uppercases %#", ({ input, expected }) => Effect.gen(function*() { assert.strictEqual(input.toUpperCase(), expected) }))Property-based tests from Schema
Section titled “Property-based tests from Schema”import { assert, it } from "@effect/vitest"import { Effect, Schema } from "effect"
// Inputs are generated from Schema arbitraries (array form -> positional,// record form -> named) and checked over many random samples.it.effect.prop("addition commutes", [Schema.Number, Schema.Number], ([a, b]) => Effect.gen(function*() { assert.strictEqual(a + b, b + a) }))Provide a test layer for a service
Section titled “Provide a test layer for a service”import { assert, it, layer } from "@effect/vitest"import { Context, Effect, Layer } from "effect"
class Greeter extends Context.Service<Greeter, { readonly hello: (name: string) => string}>()("Greeter") { static readonly layer = Layer.succeed(Greeter, { hello: (name) => `Hello, ${name}!` })}
// `layer(...)` builds the layer once and shares it across the block; the `it`// passed to the callback already has the layer's services in context.layer(Greeter.layer)("Greeter", (it) => { it.effect("greets", () => Effect.gen(function*() { const greeter = yield* Greeter assert.strictEqual(greeter.hello("Ada"), "Hello, Ada!") }))})Retry a genuinely flaky Effect
Section titled “Retry a genuinely flaky Effect”import { it } from "@effect/vitest"import { Effect } from "effect"
// `flakyTest` is a plain combinator (not an it.* definition): it retries the// Effect until it succeeds or the timeout elapses. Reach for it sparingly.it.effect("eventually succeeds", () => it.flakyTest( Effect.sync(() => { if (Math.random() > 0.1) throw new Error("retry me") }), "5 seconds" // => Duration.Input ))Capturing console output with TestConsole
Section titled “Capturing console output with TestConsole”Because it.effect provides TestConsole, any Console.log / Console.error
performed by your program is recorded instead of printed. Read it back with
TestConsole.logLines and TestConsole.errorLines from effect/testing:
import { assert, describe, it } from "@effect/vitest"import { Console, Effect } from "effect"import { TestConsole } from "effect/testing"
describe("audit", () => { it.effect("logs each step", () => Effect.gen(function*() { yield* Console.log("starting") yield* Console.error("oops")
const logs = yield* TestConsole.logLines const errors = yield* TestConsole.errorLines
assert.deepStrictEqual(logs, ["starting"]) // => captured Console.log args assert.deepStrictEqual(errors, ["oops"]) // => captured Console.error args }))})logLines and errorLines return the original arguments passed to the console
calls, flattened in call order — not formatted strings. Only code that goes
through Effect’s Console service is captured; direct globalThis.console
calls bypass the test console. TestConsole has no dedicated page; everything
it exposes (layer, make, testConsoleWith, logLines, errorLines) is
covered here.
Other @effect/vitest exports
Section titled “Other @effect/vitest exports”Beyond the it.* methods above (covered on the sub-pages), @effect/vitest
exports a few helpers for wiring and customizing your test setup. The package
also re-exports all of vitest (describe, expect, vi, beforeEach, …),
so you rarely need to import from vitest directly.
addEqualityTesters
Section titled “addEqualityTesters”Registers equality testers so Vitest’s expect(...).toEqual(...) understands
Effect’s Equal trait (e.g. two Options or Data structs compare by value).
Call it once at module top level.
import { addEqualityTesters, expect, it } from "@effect/vitest"import { Option } from "effect"
addEqualityTesters()
it("compares by Equal", () => { expect(Option.some(1)).toEqual(Option.some(1)) // => true via Equal.equals})makeMethods
Section titled “makeMethods”Builds the full set of Effect-aware methods (effect, live, layer,
flakyTest, prop, …) from a custom Vitest it. Advanced — use it when you
have a specialized Vitest TestAPI and want the @effect/vitest surface on top
of it. it itself is makeMethods(vitest.it).
import { makeMethods } from "@effect/vitest"import * as V from "vitest"
const it = makeMethods(V.it) // => { effect, live, layer, flakyTest, prop, ... }describeWrapped
Section titled “describeWrapped”Wraps a Vitest describe block and hands the callback a fresh set of
Effect-aware methods built for that block. Advanced; useful for building custom
test harnesses.
import { describeWrapped } from "@effect/vitest"import { Effect } from "effect"
describeWrapped("suite", (it) => { it.effect("runs", () => Effect.void) // => `it` is the Effect-aware methods})Assertions reference (@effect/vitest/utils)
Section titled “Assertions reference (@effect/vitest/utils)”Value-level assertion helpers, imported from @effect/vitest/utils. They throw
on failure and are meant to be called after a test has produced a value;
they do not run Effects or advance the TestClock.
import * as Assert from "@effect/vitest/utils"General assertions
Section titled “General assertions”Fails the current test with the given message.
import { fail } from "@effect/vitest/utils"
if (false) fail("unreachable branch was reached") // => throws AssertionErrorstrictEqual
Section titled “strictEqual”Reference / primitive equality via Node’s assert.strictEqual.
import { strictEqual } from "@effect/vitest/utils"
strictEqual(1 + 1, 2) // => passesdeepStrictEqual
Section titled “deepStrictEqual”Deep structural equality via Node’s assert.deepStrictEqual.
import { deepStrictEqual } from "@effect/vitest/utils"
deepStrictEqual({ a: 1 }, { a: 1 }) // => passesnotDeepStrictEqual
Section titled “notDeepStrictEqual”Asserts two values are not deeply equal.
import { notDeepStrictEqual } from "@effect/vitest/utils"
notDeepStrictEqual({ a: 1 }, { a: 2 }) // => passesassertEquals
Section titled “assertEquals”Equality using Effect’s Equal trait (falls back to a deepStrictEqual diff on
failure so Vitest can show a structural diff).
import { assertEquals } from "@effect/vitest/utils"import { Option } from "effect"
assertEquals(Option.some(1), Option.some(1)) // => passes via Equal.equalsassertTrue / assertFalse
Section titled “assertTrue / assertFalse”Assert a value is exactly true / false.
import { assertFalse, assertTrue } from "@effect/vitest/utils"
assertTrue(1 < 2) // => passesassertFalse(1 > 2) // => passesassertInclude
Section titled “assertInclude”Asserts a string includes a substring.
import { assertInclude } from "@effect/vitest/utils"
assertInclude("hello world", "world") // => passesassertMatch
Section titled “assertMatch”Asserts a string matches a regular expression.
import { assertMatch } from "@effect/vitest/utils"
assertMatch("2026-05-30", /^\d{4}-\d{2}-\d{2}$/) // => passesassertInstanceOf
Section titled “assertInstanceOf”Asserts a value is an instance of a constructor (and narrows its type).
import { assertInstanceOf } from "@effect/vitest/utils"
assertInstanceOf(new Error("boom"), Error) // => passes, narrows to ErrordoesNotThrow
Section titled “doesNotThrow”Asserts a thunk does not throw.
import { doesNotThrow } from "@effect/vitest/utils"
doesNotThrow(() => JSON.parse("{}")) // => passesthrows
Section titled “throws”Asserts a thunk throws, optionally checking the thrown value against an Error
or a validation function.
import { throws } from "@effect/vitest/utils"
throws(() => JSON.parse("{")) // => passes; threw a SyntaxErrorthrowsAsync
Section titled “throwsAsync”Like throws, but for a thunk returning a rejected promise (the only async
helper here).
import { throwsAsync } from "@effect/vitest/utils"
await throwsAsync(() => Promise.reject(new Error("nope"))) // => passesOption assertions
Section titled “Option assertions”assertSome
Section titled “assertSome”Asserts an Option is Some holding a value equal to expected.
import { assertSome } from "@effect/vitest/utils"import { Option } from "effect"
assertSome(Option.some(42), 42) // => passesassertNone
Section titled “assertNone”Asserts an Option is None.
import { assertNone } from "@effect/vitest/utils"import { Option } from "effect"
assertNone(Option.none()) // => passesassertDefined / assertUndefined
Section titled “assertDefined / assertUndefined”Assert a value is (not) undefined; assertDefined narrows the type.
import { assertDefined, assertUndefined } from "@effect/vitest/utils"
assertDefined(1 as number | undefined) // => passes, narrows to numberassertUndefined(undefined) // => passesResult assertions
Section titled “Result assertions”assertSuccess
Section titled “assertSuccess”Asserts a Result is Success holding a value equal to expected.
import { assertSuccess } from "@effect/vitest/utils"import { Result } from "effect"
assertSuccess(Result.succeed(1), 1) // => passesassertFailure
Section titled “assertFailure”Asserts a Result is Failure holding an error equal to expected.
import { assertFailure } from "@effect/vitest/utils"import { Result } from "effect"
assertFailure(Result.fail("boom"), "boom") // => passesExit assertions
Section titled “Exit assertions”assertExitSuccess
Section titled “assertExitSuccess”Asserts an Exit is a success with a value equal to expected. Pair it with
Effect.exit to capture an Effect’s outcome.
import { assertExitSuccess } from "@effect/vitest/utils"import { it } from "@effect/vitest"import { Effect } from "effect"
it.effect("succeeds", () => Effect.gen(function*() { const exit = yield* Effect.exit(Effect.succeed(1)) assertExitSuccess(exit, 1) // => passes }))assertExitFailure
Section titled “assertExitFailure”Asserts an Exit is a failure with a Cause equal to expected.
import { assertExitFailure } from "@effect/vitest/utils"import { it } from "@effect/vitest"import { Cause, Effect } from "effect"
it.effect("fails", () => Effect.gen(function*() { const exit = yield* Effect.exit(Effect.fail("boom")) assertExitFailure(exit, Cause.fail("boom")) // => passes }))The assert namespace exported from @effect/vitest also re-exports Vitest’s
own assert (assert.strictEqual, assert.deepStrictEqual, assert.isTrue,
assert.instanceOf, …), so for the most common checks you can use the single
@effect/vitest import and reach for @effect/vitest/utils when you need the
Effect-aware helpers above.
In this section
Section titled “In this section”- Writing tests — use
it.effectto run Effects, assert on results and failures, and parameterize tests witheachand property-based testing. - TestClock — drive sleeps, timeouts, retries, and schedules forward in virtual time so time-dependent code runs instantly and deterministically.
- Testing services — provide test
implementations of your services as layers, share a
layer across a block with
layer(...), and inspect state through a test ref. - Integration testing — run your real services wired together and swap only the lowest-level dependency (e.g. back the whole stack with in-memory SQLite instead of production Postgres).
- Schema and property testing —
generate inputs from
Schemaarbitraries withit.effect.prop, and validate schemas round-trip withTestSchemaandFastCheck.