TestClock
TestClock replaces live time with time controlled by your test. Use it to check sleeps, timeouts, recurring work, and clock reads without waiting for wall-clock time. Import it from effect/testing, and use Clock rather than JavaScript time reads in application code.
Configure the test runner
The starter includes Vitest 5.0.5,
@effect/vitest 4.0.5, and the local effeck-vite plugin. Run
bun run test to compile and execute src/**/*.test.effeck directly. Tests need no
prior dist/ build. Keep configuration in TypeScript:
// TypeScript (.ts)
// vitest.config.ts
import { defineConfig } from "vitest/config"
import effeck from "effeck-vite"
export default defineConfig({
plugins: [effeck()],
test: { include: ["src/**/*.test.effeck"] }
})Register it.effect at the top level or inside describe. The callback is a
program, so Effeck calls inside it run. assert and expect from
@effect/vitest are exports of an Effect package, so call them directly, as in
assert.strictEqual(actual, expected) or expect(actual).toBe(expected). A
failed assertion is a defect: it fails the test, and its stack points to the
original .effeck line through the plugin's maps. Import them from
@effect/vitest; the same functions imported from vitest are plain
TypeScript, which is EK4401 inside a test. The examples below use a local
check helper, which works the same way.
Controlling virtual time
import { TestClock } from "effect/testing"
import { it } from "@effect/vitest"
function check(condition: boolean, message: string) {
if (!condition) throw new Error(message)
}
it.effect("a deadline returns the fallback") {
const pending = fork(Effect.timeoutOrElse(() => {
sleep("10 minutes")
return Option.some("finished")
}, {
duration: "30 seconds",
orElse: () => Option.none<string>()
}))
TestClock.adjust("30 seconds")
const result = join pending
check(result.isNone(), "expected the timeout fallback")
}it.effect provides the test services, including virtual time. adjust advances by a duration and runs sleepers whose deadlines fall within that interval; setTime sets an absolute timestamp in milliseconds. Fork the timed work first so the test's own fiber remains free to adjust the clock. The block owns pending and interrupts it if the test exits early.
import { TestClock } from "effect/testing"
function standaloneClockExample() {
provide TestClock.layer() {
TestClock.setTime(5_000)
TestClock.adjust("2 seconds")
return Clock.currentTimeMillis() // 7000
}
}Outside the test harness, explicitly provide TestClock.layer(). The virtual clock does not advance merely because real time passes. Sleeping on the test's own fiber before adjusting time leaves the test waiting for an adjustment it cannot perform.
Advancing recurring computations
import { TestClock } from "effect/testing"
import { it } from "@effect/vitest"
it.effect("emits once per interval") {
const queue = Queue.unbounded<string>()
fork {
while (true) {
sleep("15 minutes")
queue.offer("refresh")
}
}
TestClock.adjust("14 minutes")
check(queue.poll().isNone(), "refresh arrived early")
TestClock.adjust("1 minute")
check(queue.take() === "refresh", "missing first refresh")
check(queue.poll().isNone(), "duplicate refresh")
TestClock.adjust("15 minutes")
check(queue.take() === "refresh", "missing second refresh")
check(queue.poll().isNone(), "duplicate second refresh")
}Use the check helper from the first example. Advancing to each boundary lets the loop schedule its next sleep before the next adjustment. The queue makes each recurrence observable without relying on real-time delays, and the background loop stops when the test block exits.
Checking clock-dependent behavior
import { TestClock } from "effect/testing"
import { it } from "@effect/vitest"
function isExpired(createdAt: number, lifetimeMillis: number) {
return Clock.currentTimeMillis() - createdAt >= lifetimeMillis
}
it.effect("expires at the boundary") {
const createdAt = Clock.currentTimeMillis()
TestClock.adjust("9 seconds")
check(!isExpired(createdAt, 10_000), "expired too soon")
TestClock.adjust("1 second")
check(isExpired(createdAt, 10_000), "should expire at ten seconds")
}Calls to Clock.currentTimeMillis() read the supplied clock and remain deterministic under the test layer. Date.now(), zero-argument new Date(), and platform timers are not application time sources in Effeck. Testing just before and at a boundary catches comparison mistakes that checking only a later timestamp misses.
Timed waits and deferred results
import { TestClock } from "effect/testing"
import { it } from "@effect/vitest"
it.effect("completes a deferred after a delay") {
const ready = Deferred.make<number>()
const worker = fork {
sleep("6 seconds")
ready.succeed(42)
}
TestClock.adjust("6 seconds")
const value = ready.await()
join worker
check(value === 42, "unexpected deferred value")
}The sleep precedes completion in the worker, so the test checks delayed completion. After advancing time, wait for the Deferred with ready.await() or join the worker to synchronize with the observable outcome. A fork alone says work has started, not that its result is ready.