Skip to content

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")
}))
})

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:

  1. Fork the time-dependent effect with Effect.forkChild so it runs on its own fiber.
  2. Adjust the clock to the time you want to simulate.
  3. Join the fiber (or otherwise observe the effect) and assert.

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())
}))
})

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)
}))
})

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.