TestClock
Most tests should run as fast as possible, and waiting for real time to pass is
both slow and flaky. TestClock replaces Effect’s Clock service with one
whose time only moves when you tell it to. Anything built on the clock —
Effect.sleep, timeouts, retries, schedules, debouncing — is
then driven deterministically by advancing virtual time, so a test of a one-hour
delay completes instantly.
it.effect provides the TestClock for you, so you just import the controls
from effect/testing and call TestClock.adjust to move time forward.
import { assert, describe, it } from "@effect/vitest"import { Effect, Fiber } from "effect"import { TestClock } from "effect/testing"
describe("TestClock", () => { it.effect("completes a sleep when time is advanced", () => Effect.gen(function*() { // Fork the sleeping effect first. The fork keeps running while we keep // control to advance the clock — otherwise the test would block here. const fiber = yield* Effect.forkChild( Effect.sleep("1 hour").pipe(Effect.as("done" as const)) )
// Move virtual time forward by one hour. Every sleep scheduled to // resume at or before the new time fires, in order. yield* TestClock.adjust("1 hour")
// Now the forked fiber has finished; join it to read its result. const value = yield* Fiber.join(fiber) assert.strictEqual(value, "done") }))})The fork-then-adjust pattern
Section titled “The fork-then-adjust pattern”This is the pattern you will use again and again. Effect.sleep (and everything
derived from it) semantically blocks until the clock reaches its scheduled
time. If you call TestClock.adjust after a sleep on the same fiber, the
sleep never gets a chance to run.
So the recipe is:
- Fork the time-dependent effect with
Effect.forkChildso it runs on its own fiber. - Adjust the clock to the time you want to simulate.
- Join the fiber (or otherwise observe the effect) and assert.
Testing timeouts
Section titled “Testing timeouts”Effect.timeoutOption returns Option.none() when the inner effect does not
complete in time. With TestClock we can drive it to either outcome
deterministically — here we advance less than the inner sleep, so it times out.
import { assert, describe, it } from "@effect/vitest"import { Effect, Fiber, Option } from "effect"import { TestClock } from "effect/testing"
describe("timeouts", () => { it.effect("times out when the work takes too long", () => Effect.gen(function*() { const fiber = yield* Effect.forkChild( // The work needs 5 minutes but is only allowed 1 minute. Effect.sleep("5 minutes").pipe(Effect.timeoutOption("1 minute")) )
// Advance past the timeout but not past the work's own duration. yield* TestClock.adjust("1 minute")
const result = yield* Fiber.join(fiber) // The timeout fired first, so the result is None. assert.deepStrictEqual(result, Option.none()) }))})Testing recurring effects
Section titled “Testing recurring effects”Schedules and repeats are also clock-driven, so you can step through a recurring effect one period at a time and assert on each occurrence. Here a value is enqueued every 60 minutes; advancing by exactly one period yields exactly one value.
import { assert, describe, it } from "@effect/vitest"import { Effect, Option, Queue } from "effect"import { TestClock } from "effect/testing"
describe("recurring effects", () => { it.effect("emits one value per period", () => Effect.gen(function*() { const queue = yield* Queue.unbounded<number>()
// Offer a value every 60 minutes, forever, on its own fiber. yield* Effect.forkChild( Queue.offer(queue, 1).pipe( Effect.delay("60 minutes"), Effect.forever ) )
// Nothing has been offered yet — no time has passed. assert.deepStrictEqual(yield* Queue.poll(queue), Option.none())
// Advance one period: exactly one value should be enqueued. yield* TestClock.adjust("60 minutes") assert.strictEqual(yield* Queue.take(queue), 1)
// And no more than one. assert.deepStrictEqual(yield* Queue.poll(queue), Option.none())
// Advance another period: the next occurrence fires. yield* TestClock.adjust("60 minutes") assert.strictEqual(yield* Queue.take(queue), 1) }))})Reading the clock and jumping to a time
Section titled “Reading the clock and jumping to a time”The TestClock starts at the Unix epoch (0). Reading the time through Clock
returns virtual time, which only changes when you adjust it. Use
TestClock.setTime to jump to an exact timestamp instead of moving by a
relative duration.
import { assert, describe, it } from "@effect/vitest"import { Clock, Duration, Effect } from "effect"import { TestClock } from "effect/testing"
describe("reading the clock", () => { it.effect("advances virtual time", () => Effect.gen(function*() { // TestClock starts at the epoch. assert.strictEqual(yield* Clock.currentTimeMillis, 0)
// `adjust` moves by a relative duration... yield* TestClock.adjust("1 minute") assert.strictEqual(yield* Clock.currentTimeMillis, 60_000)
// ...`setTime` jumps to an absolute timestamp. yield* TestClock.setTime(Duration.toMillis("2 hours")) assert.strictEqual(yield* Clock.currentTimeMillis, 7_200_000) }))})If a test uses time but never advances the TestClock, the module logs a
warning after a short delay to help you spot a hanging test — a missing
adjust call is almost always the cause.