1# @effect/vitest
2
3Helpers for testing Effect-based code with [Vitest](https://vitest.dev). Provides an enhanced `it` function with support for scoped tests, test services such as `TestClock`, shared layers, and property testing.
4
5## Installation
6
7Install Vitest 5 (`>=5.0.0 <6.0.0`) with the package as a dev dependency:
8
9```sh
10npm install -D vitest@^5 @effect/vitest
11```
12
13Vitest 5 supports Node.js `^22.12.0 || ^24.0.0 || >=26.0.0` and Vite 6.4 or later within majors 6, 7, and 8.
14
15## Links
16
17- [Website](https://effect.website): documentation, guides, and news.
18- [Reference](https://effect.website/docs/v4/api/vitest): API documentation for this package.
19- [Discord](https://discord.gg/effect-ts): ask questions, share what you're building, and talk to the core team.
20- [Community](https://effect.website/community-hub): meetups and events, or bring Effect to your own.
21- [Issues](https://github.com/Effect-TS/effect/issues): bug reports and feature requests.
22
23## Let's talk
24
25Whether your team is considering Effect, rolling it out, or already running it in production, we'd love to hear from you: what you're building, what works, and what you need from Effect next.
26
27- **Talk to the maintainers.** Introduce your team on [Discord](https://discord.gg/effect-ts) or email [contact@effectful.co](mailto:contact@effectful.co). We're happy to connect privately on Slack or Discord for feedback and help with adoption.
28- **Production support.** We're exploring how to better support teams running Effect in production. If your organization has specific support needs, let's discuss them.
29- **Adoption help.** Our [adoption partners](https://effect.website/adoption-partners) offer implementation, consulting, team extension, training, and commercial support.
30
31## Migrating to Vitest 5
32
33The package re-exports Vitest's public API. Upgrading removes the same exports and helpers that Vitest 5 removes; it does not provide compatibility shims.
34
35- Replace `test.sequential`, `it.sequential`, `describe.sequential`, and `{ sequential: true }` with `{ concurrent: false }`. Set this explicitly for suites that share mutable state or depend on test order when concurrency is enabled.
36- Replace the top-level `bench` import with the test-context fixture:
37
38 ```ts
39 import { test } from "@effect/vitest"
40
41 test("sort", async ({ bench }) => {
42 await bench("sort", () => [3, 1, 2].sort()).run()
43 })
44 ```
45
46 Use `test.skip`, `test.only`, or `test.todo` on the enclosing test. The old `BenchFactory`, `BenchFunction`, `BenchTask`, `BenchTaskResult`, `Benchmark`, `BenchmarkAPI`, `BenchmarkResult`, and `BenchmarkRunner` exports are removed. Use the fixture's `Bench`, `BenchFn`, `BenchRegistration`, and `BenchResult` types as appropriate; custom benchmark engines use `BenchmarkProvider`.
47- Change `Assertion<T>` to `Assertion<void, T>` for synchronous assertions or `Assertion<Promise<void>, T>` for asynchronous assertions. Augment `Matchers<R, T>` in `vitest` for custom matchers. Vitest's assertion state is no longer shared with `@vitest/expect`.
48- The `ExpectPollOptions` export is removed. Derive the options type with `NonNullable<Parameters<typeof expect.poll>[1]>` when needed.
49- Import reporter types from `vitest/node` and environment or snapshot APIs from `vitest/runtime`.
50
51Vitest 5 clears mock call history before each test by default and requires asynchronous assertions to be awaited. JSON reporters write to a file by default; use an explicit `outputFile` when consuming their results. See the [Vitest migration guide](https://vitest.dev/guide/migration/) for the remaining upstream changes.
52
53The Effect helpers retain their existing calling convention: `it.effect(name, effect, options)`, `it.live(name, effect, options)`, shared layers, and property tests.
54
55Both `layer` and `it.layer` accept `{ concurrent: false }` to serialize a named shared-layer suite, or `{ concurrent: true }` to run its tests concurrently. Omitting the option inherits suite concurrency; nested named layers can override it. Anonymous layers always inherit the enclosing suite's concurrency, regardless of the option.
56
57In concurrent tests, use the callback's `ctx.expect` so snapshots and assertion counts belong to the right test.
58
59## Overview
60
61The main entry point is the following import:
62
63```ts
64import { it } from "@effect/vitest"
65```
66
67This import enhances the standard `it` function from `vitest` with several powerful features, including:
68
69| Feature | Description |
70| -------------- | --------------------------------------------------------------------------------------------------- |
71| `it.effect` | Runs a scoped test with test services such as `TestClock` and `TestConsole`. |
72| `it.live` | Runs a scoped test with the live Effect environment. |
73| `it.layer` | Shares a `Layer` between multiple tests. |
74| `it.prop` | Runs property tests using Effect `Schema` and `Arbitrary` values. |
75| `it.flakyTest` | Retries an Effect that might occasionally fail until it succeeds or reaches the configured timeout. |
76
77Property tests shrink callbacks that return `false`, throw, or complete with a non-interruption Effect failure. This
78includes failed assertions, typed failures, and defects. Effect interruption still interrupts the test. Returning
79normally with any value other than `false`, including `void`, passes for that generated input.
80
81The Vitest `timeout` interrupts the Effect fiber running property generation, evaluation, and shrinking. Effect
82finalizers run during the interruption, which is reported as a test timeout rather than a property falsification. As
83with other Effect programs, a timeout cannot preempt a synchronous JavaScript callback that does not return.
84
85## Writing Tests with `it.effect`
86
87Here's how to use `it.effect` to write your tests:
88
89**Syntax**
90
91```ts
92import { it } from "@effect/vitest"
93
94it.effect("test name", () => EffectContainingAssertions, timeout: number | TestOptions = 5_000)
95```
96
97`it.effect` automatically provides the Effect test services, including [`TestClock`](#using-the-testclock), and a fresh `Scope` for each test. The scope is closed when the test finishes.
98
99### Testing Successful Operations
100
101To write a test, place your assertions directly within the main effect. This ensures that your assertions are evaluated as part of the test's execution.
102
103**Example** (Testing a Successful Operation)
104
105In the following example, we test a function that divides two numbers, but fails if the divisor is zero. The goal is to check that the function returns the correct result when given valid input.
106
107```ts
108import { expect, it } from "@effect/vitest"
109import { Effect } from "effect"
110
111// A simple divide function that returns an Effect, failing when dividing by zero
112function divide(a: number, b: number) {
113 if (b === 0) return Effect.fail("Cannot divide by zero")
114 return Effect.succeed(a / b)
115}
116
117// Testing a successful division
118it.effect("test success", () =>
119 Effect.gen(function*() {
120 const result = yield* divide(4, 2) // Expect 4 divided by 2 to succeed
121 expect(result).toBe(2) // Assert that the result is 2
122 }))
123```
124
125### Testing Successes and Failures as `Exit`
126
127When you need to handle both success and failure cases in a test, you can use `Effect.exit` to capture the outcome as an `Exit` object. This allows you to verify both successful and failed results within the same test structure.
128
129**Example** (Testing Success and Failure with `Exit`)
130
131```ts
132import { expect, it } from "@effect/vitest"
133import { Effect, Exit } from "effect"
134
135// A function that divides two numbers and returns an Effect.
136// It fails if the divisor is zero.
137function divide(a: number, b: number) {
138 if (b === 0) return Effect.fail("Cannot divide by zero")
139 return Effect.succeed(a / b)
140}
141
142// Test case for a successful division, using `Effect.exit` to capture the result
143it.effect("test success as Exit", () =>
144 Effect.gen(function*() {
145 const result = yield* Effect.exit(divide(4, 2)) // Capture the result as an Exit
146 expect(result).toStrictEqual(Exit.succeed(2)) // Expect success with the value 2
147 }))
148
149// Test case for a failure (division by zero), using `Effect.exit`
150it.effect("test failure as Exit", () =>
151 Effect.gen(function*() {
152 const result = yield* Effect.exit(divide(4, 0)) // Capture the result as an Exit
153 expect(result).toStrictEqual(Exit.fail("Cannot divide by zero")) // Expect failure with the correct message
154 }))
155```
156
157### Using the TestClock
158
159When writing tests with `it.effect`, Effect test services are automatically provided. These include the [`TestClock`](https://effect.website/docs/guides/testing/testclock), which allows you to simulate the passage of time in your tests.
160
161**Note**: If you want to use the real-time clock (instead of the simulated one), you can switch to `it.live`.
162
163**Example** (Using `TestClock` and `it.live`)
164
165Here are examples that demonstrate how you can work with time in your tests using `it.effect` and `TestClock`:
166
1671. **Using `it.live` to show the current time**: This will display the actual system time, since it runs in the live environment.
168
1692. **Using `it.effect` without adjustments**: By default, the `TestClock` starts at `0`, simulating the beginning of time for your test without any time passing.
170
1713. **Using `it.effect` and adjusting time**: In this test, we simulate the passage of time by advancing the clock by 1000 milliseconds (1 second).
172
173```ts
174import { it } from "@effect/vitest"
175import { Clock, Effect } from "effect"
176import { TestClock } from "effect/testing"
177
178// Effect to log the current time
179const logNow = Effect.gen(function*() {
180 const now = yield* Clock.currentTimeMillis // Fetch the current time from the clock
181 console.log(now) // Log the current time
182})
183
184// Example of using the real system clock with `it.live`
185it.live("runs the test with the live Effect environment", () =>
186 Effect.gen(function*() {
187 yield* logNow // Prints the actual current time
188 }))
189
190// Example of using `it.effect` with the default test environment
191it.effect("run the test with the test environment", () =>
192 Effect.gen(function*() {
193 yield* logNow // Prints 0, as the test clock starts at 0
194 }))
195
196// Example of advancing the test clock by 1000 milliseconds
197it.effect("run the test with the test environment and the time adjusted", () =>
198 Effect.gen(function*() {
199 yield* TestClock.adjust("1000 millis") // Move the clock forward by 1000 milliseconds
200 yield* logNow // Prints 1000, reflecting the adjusted time
201 }))
202```
203
204### Skipping Tests
205
206If you need to temporarily disable a test but don't want to delete or comment out the code, you can use `it.effect.skip`. This is helpful when you're working on other parts of your test suite but want to keep the test for future execution.
207
208**Example** (Skipping a Test)
209
210```ts
211import { it } from "@effect/vitest"
212import { expect } from "@effect/vitest"
213import { Effect, Exit } from "effect"
214
215function divide(a: number, b: number) {
216 if (b === 0) return Effect.fail("Cannot divide by zero")
217 return Effect.succeed(a / b)
218}
219
220// Temporarily skip the test for dividing numbers
221it.effect.skip("test failure as Exit", () =>
222 Effect.gen(function*() {
223 const result = yield* Effect.exit(divide(4, 0))
224 expect(result).toStrictEqual(Exit.fail("Cannot divide by zero"))
225 }))
226```
227
228### Running a Single Test
229
230When you're developing or debugging, it's often useful to run a specific test without executing the entire test suite. You can achieve this by using `it.effect.only`, which will run just the selected test and ignore the others.
231
232**Example** (Running a Single Test)
233
234```ts
235import { it } from "@effect/vitest"
236import { expect } from "@effect/vitest"
237import { Effect, Exit } from "effect"
238
239function divide(a: number, b: number) {
240 if (b === 0) return Effect.fail("Cannot divide by zero")
241 return Effect.succeed(a / b)
242}
243
244// Run only this test, skipping all others
245it.effect.only("test failure as Exit", () =>
246 Effect.gen(function*() {
247 const result = yield* Effect.exit(divide(4, 0))
248 expect(result).toStrictEqual(Exit.fail("Cannot divide by zero"))
249 }))
250```
251
252### Expecting Tests to Fail
253
254When adding new failing tests, you might not be able to fix them right away. Instead of skipping them, you may want to assert it fails, so that when you fix them, you'll know and can re-enable them before it regresses.
255
256**Example** (Asserting one test fails)
257
258```ts
259import { it } from "@effect/vitest"
260import { Effect, Exit } from "effect"
261
262function divide(a: number, b: number) {
263 if (b === 0) return Effect.fail("Cannot divide by zero")
264 return Effect.succeed(a / b)
265}
266
267// Temporarily assert that the test for dividing by zero fails.
268it.effect.fails("dividing by zero special cases", ({ expect }) =>
269 Effect.gen(function*() {
270 const result = yield* Effect.exit(divide(4, 0))
271 expect(result).toStrictEqual(0)
272 }))
273```
274
275### Logging
276
277By default, `it.effect` suppresses log output, which can be useful for keeping test results clean. However, if you want to enable logging during tests, you can use `it.live` or provide a custom logger to control the output.
278
279**Example** (Controlling Logging in Tests)
280
281```ts
282import { it } from "@effect/vitest"
283import { Effect, Logger } from "effect"
284
285// This test won't display the log message, as logging is suppressed by default in `it.effect`
286it.effect("does not display a log", () =>
287 Effect.gen(function*() {
288 yield* Effect.log("it.effect") // Log won't be shown
289 }))
290
291// This test will display the log because a custom logger is provided
292it.effect("providing a logger displays a log", () =>
293 Effect.gen(function*() {
294 yield* Effect.log("it.effect with custom logger") // Log will be displayed
295 }).pipe(
296 Effect.provide(Logger.layer([Logger.consolePretty()])) // Providing a pretty logger for log output
297 ))
298
299// This test runs using `it.live`, which enables logging by default
300it.live("it.live displays a log", () =>
301 Effect.gen(function*() {
302 yield* Effect.log("it.live") // Log will be displayed
303 }))
304```
305
306## Resource Safety and Scope
307
308Both `it.effect` and `it.live` provide a fresh `Scope` and close it after each test. Test bodies can therefore use scoped resources directly. Do not wrap the test body in `Effect.scoped`, because the test runner already manages its scope.
309
310**Example** (Managing a Resource Lifecycle)
311
312```ts
313import { it } from "@effect/vitest"
314import { Console, Effect } from "effect"
315
316// Simulating the acquisition and release of a resource with console logging
317const acquire = Console.log("acquire resource")
318const release = Console.log("release resource")
319
320// Defining a resource that requires proper management
321const resource = Effect.acquireRelease(acquire, () => release)
322
323it.effect("run with scope", () =>
324 Effect.gen(function*() {
325 yield* resource
326 }))
327```
328
329## Using Vitest Fixtures
330
331To use [Vitest fixtures](https://vitest.dev/guide/test-context#extend-test-context) in Effect tests, pass a test extended with `test.extend` to `makeMethods`. It returns the same helpers as `it`, and each test receives the fixtures it destructures from its context.
332
333**Example** (Sharing a Database Across a Test File)
334
335```ts
336import { assert, makeMethods, test } from "@effect/vitest"
337import { PGlite } from "@electric-sql/pglite"
338import { Effect } from "effect"
339
340const it = makeMethods(
341 test.extend("db", { scope: "file" }, async ({}, { onCleanup }) => {
342 const db = await PGlite.create()
343 onCleanup(() => db.close())
344 return db
345 })
346)
347
348it.effect("runs a query", ({ db }) =>
349 Effect.gen(function*() {
350 const result = yield* Effect.promise(() => db.query("select 1 as one"))
351 assert.deepStrictEqual(result.rows, [{ one: 1 }])
352 }))
353```
354
355Destructure the fixtures a test uses: Vitest reads those names to decide what to set up, and rejects `(ctx) =>` once any fixture is defined. `it.effect.each` passes the context after the test case. Property tests cannot request fixtures, but auto fixtures still run. Build `makeMethods` from `test` or `test.extend(...)`, not from the test a `describe` callback receives: that API is bound to the outer suite, so tests in a named `it.layer` can end up outside their named suite and miss its hooks and concurrency setting.
356
357## Writing Tests with `it.flakyTest`
358
359`it.flakyTest` is a utility designed to manage tests that may not succeed consistently on the first attempt. These tests, often referred to as "flaky," can fail due to factors like timing issues, external dependencies, or randomness. `it.flakyTest` allows for retrying these tests until they pass or a specified timeout is reached.
360
361**Example** (Handling Flaky Tests with Retries)
362
363Let's start by setting up a basic test scenario that has the potential to fail randomly:
364
365```ts
366import { it } from "@effect/vitest"
367import { Effect, Random } from "effect"
368
369// Simulating a flaky effect
370const flaky = Effect.gen(function*() {
371 const random = yield* Random.nextBoolean
372 if (random) {
373 return yield* Effect.fail("Failed due to randomness")
374 }
375})
376
377// Standard test that may fail intermittently
378it.effect("possibly failing test", () => flaky)
379```
380
381In this test, the outcome is random, so the test might fail depending on the result of `Random.nextBoolean`.
382
383To handle this flakiness, we use `it.flakyTest` to retry the test until it passes, or until a defined timeout expires:
384
385```ts
386// Retrying the flaky test with a 5-second timeout
387it.effect("retrying until success or timeout", () => it.flakyTest(flaky, "5 seconds"))
388```