Skip to main content
A comprehensive testing library for Effect-based applications using Vitest, providing utilities for testing Effects with automatic resource cleanup, test clocks, and layer management.

Installation

pnpm add -D vitest @effect/vitest

Basic Usage

Import the enhanced it function from @effect/vitest:
import { it, expect } from "@effect/vitest"
import { Effect } from "effect"

Testing Effects

Use it.effect to write tests for Effect programs:
import { it, expect } from "@effect/vitest"
import { Effect } from "effect"

function divide(a: number, b: number) {
  if (b === 0) return Effect.fail("Cannot divide by zero")
  return Effect.succeed(a / b)
}

it.effect("divides two numbers", () =>
  Effect.gen(function*() {
    const result = yield* divide(10, 2)
    expect(result).toBe(5)
  })
)

Testing Success and Failure

Use Effect.exit to test both success and failure cases:
import { it, expect } from "@effect/vitest"
import { Effect, Exit } from "effect"

it.effect("handles success", () =>
  Effect.gen(function*() {
    const result = yield* Effect.exit(divide(10, 2))
    expect(result).toStrictEqual(Exit.succeed(5))
  })
)

it.effect("handles division by zero", () =>
  Effect.gen(function*() {
    const result = yield* Effect.exit(divide(10, 0))
    expect(result).toStrictEqual(Exit.fail("Cannot divide by zero"))
  })
)

Test Clock

it.effect automatically provides a TestContext with a TestClock for simulating time:
import { it } from "@effect/vitest"
import { Clock, Effect, TestClock } from "effect"

it.effect("tests with simulated time", () =>
  Effect.gen(function*() {
    const start = yield* Clock.currentTimeMillis
    console.log(start) // 0
    
    yield* TestClock.adjust("1 second")
    
    const after = yield* Clock.currentTimeMillis
    console.log(after) // 1000
  })
)

Live Environment

Use it.live to run tests with the real-time clock:
import { it } from "@effect/vitest"
import { Clock, Effect } from "effect"

it.live("uses real time", () =>
  Effect.gen(function*() {
    const now = yield* Clock.currentTimeMillis
    console.log(now) // Actual system time
  })
)

Scoped Tests

Use it.scoped for tests that require resource management:
import { it } from "@effect/vitest"
import { Console, Effect } from "effect"

it.scoped("manages resources", () =>
  Effect.gen(function*() {
    const resource = yield* Effect.acquireRelease(
      Console.log("acquire"),
      () => Console.log("release")
    )
    
    // Resource is automatically released after the test
  })
)

Layer Testing

Share layers between tests using it.layer:
import { it, layer } from "@effect/vitest"
import { Effect, Layer, ServiceMap } from "effect"

class Database extends ServiceMap.Service("Database")<Database, {
  query: (sql: string) => Effect.Effect<string>
}>() {
  static Live = Layer.succeed(Database, {
    query: (sql) => Effect.succeed(`Result for: ${sql}`)
  })
}

layer(Database.Live)("database tests", (it) => {
  it.effect("queries the database", () =>
    Effect.gen(function*() {
      const db = yield* Database
      const result = yield* db.query("SELECT * FROM users")
      expect(result).toContain("Result for")
    })
  )
  
  it.effect("performs another query", () =>
    Effect.gen(function*() {
      const db = yield* Database
      const result = yield* db.query("SELECT * FROM posts")
      expect(result).toContain("Result for")
    })
  )
})

Nested Layers

Layers can be nested for more complex test scenarios:
import { layer } from "@effect/vitest"
import { Effect, Layer, ServiceMap } from "effect"

class Config extends ServiceMap.Service("Config")<Config, { apiUrl: string }>() {
  static Live = Layer.succeed(Config, { apiUrl: "https://api.example.com" })
}

class ApiClient extends ServiceMap.Service("ApiClient")<ApiClient, {
  fetch: (path: string) => Effect.Effect<string>
}>() {
  static Live = Layer.effect(ApiClient,
    Effect.gen(function*() {
      const config = yield* Config
      return {
        fetch: (path) => Effect.succeed(`${config.apiUrl}${path}`)
      }
    })
  )
}

layer(Config.Live)("with config", (it) => {
  it.layer(ApiClient.Live)("with api client", (it) => {
    it.effect("makes API calls", () =>
      Effect.gen(function*() {
        const client = yield* ApiClient
        const result = yield* client.fetch("/users")
        expect(result).toBe("https://api.example.com/users")
      })
    )
  })
})

Property-Based Testing

Use it.prop for property-based testing with FastCheck:
import { it } from "@effect/vitest"
import { Effect, Schema } from "effect"

it.effect.prop(
  "string length is always positive",
  { str: Schema.String },
  ({ str }) =>
    Effect.gen(function*() {
      expect(str.length).toBeGreaterThanOrEqual(0)
    })
)

it.effect.prop(
  "addition is commutative",
  { a: Schema.Number, b: Schema.Number },
  ({ a, b }) =>
    Effect.gen(function*() {
      expect(a + b).toBe(b + a)
    })
)

Flaky Tests

Handle tests that may occasionally fail:
import { it, flakyTest } from "@effect/vitest"
import { Effect, Random } from "effect"

const flaky = Effect.gen(function*() {
  const random = yield* Random.nextBoolean
  if (!random) {
    return yield* Effect.fail("Random failure")
  }
})

it.effect("retries until success", () =>
  flakyTest(flaky, "5 seconds")
)

Test Modifiers

Skip Tests

it.effect.skip("skipped test", () =>
  Effect.void
)

Run Only Specific Tests

it.effect.only("only this test runs", () =>
  Effect.void
)

Skip Conditionally

it.effect.skipIf(process.env.CI)("skip in CI", () =>
  Effect.void
)

Run Conditionally

it.effect.runIf(!process.env.CI)("run locally only", () =>
  Effect.void
)

Expect Failure

it.effect.fails("this test should fail", () =>
  Effect.die("Expected failure")
)

Parameterized Tests

Run the same test with different inputs:
import { it, expect } from "@effect/vitest"
import { Effect } from "effect"

it.effect.each([
  { input: 2, expected: 4 },
  { input: 3, expected: 9 },
  { input: 4, expected: 16 }
])("squares $input to get $expected", ({ input, expected }) =>
  Effect.gen(function*() {
    const result = input * input
    expect(result).toBe(expected)
  })
)

Logging in Tests

By default, logs are suppressed. Enable them when needed:
import { it } from "@effect/vitest"
import { Effect, Logger } from "effect"

// Logs are suppressed
it.effect("no logs", () =>
  Effect.log("This won't be shown")
)

// Provide a logger to see logs
it.effect("with logs", () =>
  Effect.log("This will be shown").pipe(
    Effect.provide(Logger.pretty)
  )
)

// Use it.live to enable logs by default
it.live("live with logs", () =>
  Effect.log("This will be shown")
)

Equality Testers

Add custom equality testers for Effect types:
import { addEqualityTesters } from "@effect/vitest"

// Call once in your test setup
addEqualityTesters()

API Reference

  • it.effect: Test Effect programs with TestContext
  • it.live: Test with live environment (real time)
  • it.scoped: Test with Scope for resource management
  • it.layer: Share layers between tests
  • it.prop: Property-based testing
  • flakyTest: Retry tests until success or timeout
  • addEqualityTesters: Add Effect equality testers to Vitest

Build docs developers (and LLMs) love