Writing Tests
The core idea of @effect/vitest is that a test returns an Effect. You write
the body with Effect.gen, yield* the effects you want to exercise, and use
assert to check the values. it.effect runs the Effect, fails the test if it
fails unexpectedly, provides the test services (TestClock, TestConsole), and
closes the test Scope afterwards.
import { assert, describe, it } from "@effect/vitest"import { Effect } from "effect"
// The code under test: a small Effect-returning function.const divide = (a: number, b: number): Effect.Effect<number, "DivByZero"> => b === 0 ? Effect.fail("DivByZero") : Effect.succeed(a / b)
describe("divide", () => { // The body is an Effect. `yield*` runs the effect under test, then we // assert on the value it produced. it.effect("returns the quotient on success", () => Effect.gen(function*() { const result = yield* divide(10, 2) assert.strictEqual(result, 5) }))})Assertions
Section titled “Assertions”Import assert from @effect/vitest. It re-exports Vitest’s assert, so all
the familiar checks are available:
import { assert } from "@effect/vitest"
assert.strictEqual(1 + 1, 2) // reference / primitive equalityassert.deepStrictEqual([1, 2], [1, 2]) // structural equalityassert.isTrue([1, 2].includes(1)) // boolean checksassert.include("hello world", "world") // substring / element membershipFor Effect data types there are dedicated helpers in @effect/vitest/utils that
give clearer failure messages than unwrapping by hand — assertSome /
assertNone for Option, assertSuccess / assertFailure for
Result, and assertExitSuccess / assertExitFailure for
Exit.
Testing failures
Section titled “Testing failures”An Effect that fails would fail the surrounding test, which is not what you
want when the failure is the expected outcome. Capture the outcome with
Effect.exit (or Effect.result) and assert on it instead.
import { assert, describe, it } from "@effect/vitest"import { Effect, Exit } from "effect"
const divide = (a: number, b: number): Effect.Effect<number, "DivByZero"> => b === 0 ? Effect.fail("DivByZero") : Effect.succeed(a / b)
describe("divide failures", () => { it.effect("fails with DivByZero when dividing by zero", () => Effect.gen(function*() { // `Effect.exit` turns failure into a value, so the test does not abort. const exit = yield* Effect.exit(divide(1, 0)) assert.deepStrictEqual(exit, Exit.fail("DivByZero")) }))})Exit distinguishes an expected fail (your typed error channel) from a
die (an unexpected defect). Asserting on the whole Exit therefore checks not
just that it failed but how it failed.
Parameterized tests with each
Section titled “Parameterized tests with each”it.effect.each runs the same Effect body once per row in a table — ideal for
table-driven tests. The %# placeholder in the test name is replaced with the
case index.
import { assert, describe, it } from "@effect/vitest"import { Effect } from "effect"
describe("normalize", () => { it.effect.each([ { input: " Ada ", expected: "ada" }, { input: " Lin ", expected: "lin" }, { input: " Nia ", expected: "nia" } ])("trims and lowercases case %#", ({ input, expected }) => Effect.gen(function*() { assert.strictEqual(input.trim().toLowerCase(), expected) }))})Property-based testing
Section titled “Property-based testing”it.effect.prop generates inputs and runs the Effect body many times with
different values, shrinking to a minimal counterexample on failure. Inputs are
described with Schema, which doubles as an arbitrary generator.
import { assert, describe, it } from "@effect/vitest"import { Effect, Schema } from "effect"
describe("string properties", () => { // For each generated string, reversing it twice must be the identity. it.effect.prop( "reversing twice is identity", [Schema.String], ([value]) => Effect.gen(function*() { const reversedTwice = value.split("").reverse().reverse().join("") assert.strictEqual(reversedTwice, value) }) )})it.effect vs it.live
Section titled “it.effect vs it.live”it.effect runs your test against the test services: a TestClock whose
time only advances when you tell it to, and a TestConsole that captures output
instead of printing it. This is what makes time-dependent tests deterministic
(see TestClock).
Occasionally you genuinely want the real runtime — the system clock, real
delays. Use it.live for those cases.
import { assert, describe, it } from "@effect/vitest"import { Clock, Effect } from "effect"
describe("live runtime", () => { // `it.live` uses the real Clock, so `Effect.sleep` actually waits. it.live("sleeps against the real clock", () => Effect.gen(function*() { const before = yield* Clock.currentTimeMillis yield* Effect.sleep("10 millis") const after = yield* Clock.currentTimeMillis assert.isTrue(after - before >= 10) }))})Flaky tests
Section titled “Flaky tests”For tests that depend on something genuinely non-deterministic, wrap the body
with it.flakyTest, which retries the Effect until it succeeds or a timeout
elapses.
import { describe, it } from "@effect/vitest"import { Effect, Random } from "effect"
describe("occasionally flaky", () => { it.effect("eventually rolls a six", () => // Retry the Effect for up to 1 second until it succeeds. it.flakyTest( Effect.gen(function*() { // `nextIntBetween` is inclusive on both bounds in v4, so this is 1-6. const roll = yield* Random.nextIntBetween(1, 6) // Fail unless we rolled a six — `flakyTest` will retry on failure. yield* Effect.filterOrFail( Effect.succeed(roll), (n) => n === 6, () => "not a six" as const ) }), "1 second" ))})Reach for flakyTest sparingly — most of the time the better fix is to make the
test deterministic with a TestClock, a seeded Random, or a test layer rather
than to retry it.