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.
Using TestClock without @effect/vitest
Section titled “Using TestClock without @effect/vitest”it.effect provides the TestClock for you, but the module is just an ordinary
service: you can drive virtual time in any program by providing
TestClock.layer() and reaching for the same adjust / setTime controls. The
layer constructs a TestClock and installs it as the Clock service, so every
clock-aware operator (Effect.sleep, timeouts, schedules) becomes virtual.
import { Effect, Fiber } from "effect"import { TestClock } from "effect/testing"
const program = Effect.gen(function*() { // Fork the sleeping work so we keep control to advance the clock. const fiber = yield* Effect.forkChild( Effect.sleep("1 hour").pipe(Effect.as("done" as const)) )
// Move virtual time forward — the forked sleep resumes immediately. yield* TestClock.adjust("1 hour")
return yield* Fiber.join(fiber)})
// Provide the TestClock layer instead of relying on `it.effect`.Effect.runPromise(program.pipe(Effect.provide(TestClock.layer()))) .then(console.log) // => "done"Reference
Section titled “Reference”Everything below is exported from effect/testing’s TestClock module. The
top-level adjust, setTime, and withLive functions read the active
TestClock out of the fiber context, so they work directly inside it.effect
or any program that provided TestClock.layer().
adjust
Section titled “adjust”adjust(duration: Duration.Input) increments virtual time by a relative
duration. Every sleep scheduled to resume at or before the new time fires, in
clock-time order. This is the workhorse covered in
the fork-then-adjust pattern above.
import { Clock, Effect } from "effect"import { TestClock } from "effect/testing"
const program = Effect.gen(function*() { yield* TestClock.adjust("90 minutes") return yield* Clock.currentTimeMillis // => 5400000})setTime
Section titled “setTime”setTime(timestamp: number) jumps virtual time to an absolute epoch-millis
timestamp (rather than moving by a relative amount). Sleeps due at or before the
target still fire in order.
import { Clock, Duration, Effect } from "effect"import { TestClock } from "effect/testing"
const program = Effect.gen(function*() { // Jump straight to "2 hours since the epoch". yield* TestClock.setTime(Duration.toMillis("2 hours")) return yield* Clock.currentTimeMillis // => 7200000})withLive
Section titled “withLive”withLive(effect) runs a single effect against the live system Clock
while the rest of the program stays on virtual time. Use it when you genuinely
need real elapsed time — e.g. measuring wall-clock latency or live logging.
import { Clock, Effect } from "effect"import { TestClock } from "effect/testing"
const program = Effect.gen(function*() { const virtual = yield* Clock.currentTimeMillis // => 0 (TestClock at epoch) const real = yield* TestClock.withLive(Clock.currentTimeMillis) // real => actual system timestamp, e.g. 1748563200000 return { virtual, real }})testClockWith
Section titled “testClockWith”testClockWith(f) hands you the underlying TestClock service so you can call
its methods (adjust, setTime, currentTimeMillisUnsafe, withLive)
directly. The convenience exports adjust / setTime / withLive are thin
wrappers over this.
import { Effect } from "effect"import { TestClock } from "effect/testing"
const program = TestClock.testClockWith((clock) => Effect.sync(() => clock.currentTimeMillisUnsafe()) // => 0)make(options?) constructs a TestClock service value as an effect. The only
option is warningDelay (a Duration.Input, default "1 second"), which
controls how long before the hanging-test warning is logged. The returned
service implements the full Clock interface plus the
test-only controls:
currentTimeMillis/currentTimeMillisUnsafe()— read virtual time in millis.currentTimeNanos/currentTimeNanosUnsafe()— read virtual time in nanos.adjust(duration)— advance by a relative duration.setTime(timestamp)— jump to an absolute timestamp.sleep(duration)— the clock-aware sleep that the rest of Effect builds on.withLive(effect)— run an effect against the live clock.
import { Effect } from "effect"import { TestClock } from "effect/testing"
const program = Effect.gen(function*() { const clock = yield* TestClock.make({ warningDelay: "5 seconds" }) yield* clock.adjust("1 hour") return clock.currentTimeMillisUnsafe() // => 3600000})layer(options?) returns a Layer.Layer<TestClock> that builds a TestClock
and installs it as the Clock service. Provide it to any program that does not
go through it.effect (which already provides one for you).
import { Effect } from "effect"import { TestClock } from "effect/testing"
const program = Effect.gen(function*() { yield* TestClock.adjust("1 day")})
// Manually provide the layer (optionally configuring the warning delay).Effect.runPromise( program.pipe(Effect.provide(TestClock.layer({ warningDelay: "10 seconds" }))))TestClock.Options and TestClock.State
Section titled “TestClock.Options and TestClock.State”The TestClock namespace exposes two supporting types. TestClock.Options is
the { readonly warningDelay?: Duration.Input } shape accepted by make and
layer. TestClock.State ({ timestamp: number; sleeps: ReadonlyArray<[number, Latch.Latch]> })
describes the internal state a clock tracks — the current virtual timestamp and
the pending sleeps waiting to resume.
import type { Duration } from "effect"import type { TestClock } from "effect/testing"
// A typed options value you can pass to make/layer.const options: TestClock.Options = { warningDelay: "2 seconds" }const delay: Duration.Input | undefined = options.warningDelay // => "2 seconds"