Claude Code Plugins

Community-maintained marketplace

Feedback

Write comprehensive tests using @effect/vitest for Effect code and vitest for pure functions. Use this skill when implementing tests for Effect-based applications, including services, layers, time-dependent effects, error handling, and property-based testing.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name effect-testing
description Write comprehensive tests using @effect/vitest for Effect code and vitest for pure functions. Use this skill when implementing tests for Effect-based applications, including services, layers, time-dependent effects, error handling, and property-based testing.

Effect Testing Skill

This skill provides comprehensive guidance for testing Effect-based applications using @effect/vitest and standard vitest.

Framework Selection

CRITICAL: Choose the correct testing framework based on the code being tested.

Use @effect/vitest for Effect Code

Use @effect/vitest when testing:

  • Functions that return Effect<A, E, R>
  • Code that uses services and layers
  • Time-dependent operations with TestClock
  • Asynchronous operations coordinated with Effect
  • STM (Software Transactional Memory) operations
import { it, expect } from "@effect/vitest"
import { Effect } from "effect"

it.effect("should fetch user", () =>
  Effect.gen(function* () {
    const user = yield* fetchUser("123")
    expect(user.id).toBe("123")
  })
)

Use Regular vitest for Pure Functions

Use standard vitest for:

  • Pure functions with no Effect wrapper
  • Simple data transformations
  • Helper utilities
  • Type constructors (brands, newtypes)
import { describe, expect, it } from "vitest"

describe("Cents", () => {
  it("should add cents correctly", () => {
    const result = Cents.add(Cents.make(100n), Cents.make(50n))
    expect(result).toBe(150n)
  })
})

Test Variants

it.effect - Default Test Environment

Provides TestContext including TestClock, TestRandom, etc.

it.effect("test name", () =>
  Effect.gen(function* () {
    // Test implementation with TestContext available
    const result = yield* someEffect
    expect(result).toBe(expected)
  })
)

it.live - Live Environment

Uses real services (real clock, real random, etc.).

it.live("test with real time", () =>
  Effect.gen(function* () {
    const now = yield* Clock.currentTimeMillis
    // Uses actual system time
  })
)

it.scoped - Resource Management

For tests requiring Scope to manage resource lifecycle.

it.scoped("test with resources", () =>
  Effect.gen(function* () {
    const resource = yield* Effect.acquireRelease(
      acquire,
      () => release
    )
    // Resource automatically cleaned up after test
  })
)

it.scopedLive - Combined Scoped + Live

Uses live environment with scope for resource management.

it.scopedLive("live test with resources", () =>
  Effect.gen(function* () {
    const resource = yield* Effect.acquireRelease(
      acquireRealResource,
      () => releaseRealResource
    )
  })
)

Assertions

Use expect from vitest

For all assertions, use the standard expect from vitest:

import { expect } from "@effect/vitest"

it.effect("assertions", () =>
  Effect.gen(function* () {
    const result = yield* computation
    expect(result).toBe(42)
    expect(result).toBeGreaterThan(0)
    expect(array).toHaveLength(3)
  })
)

Effect-Specific Utilities

@effect/vitest provides additional assertion utilities in utils:

import {
  assertEquals,       // Uses Effect's Equal.equals
  assertTrue,
  assertFalse,
  assertSome,        // For Option.Some
  assertNone,        // For Option.None
  assertRight,       // For Either.Right
  assertLeft,        // For Either.Left
  assertSuccess,     // For Exit.Success
  assertFailure      // For Exit.Failure
} from "@effect/vitest/utils"

it.effect("with effect assertions", () =>
  Effect.gen(function* () {
    const option = yield* someOptionalEffect
    assertSome(option, expectedValue)

    const either = yield* someEitherEffect
    assertRight(either, expectedValue)
  })
)

Testing with Services and Layers

Providing Services to Tests

Use Effect.provide to supply test implementations:

it.effect("should work with dependencies", () =>
  Effect.gen(function* () {
    const result = yield* UserService.getUser("123")
    expect(result.name).toBe("John")
  }).pipe(Effect.provide(TestUserServiceLayer))
)

Using layer Helper

Share a layer across multiple tests with the layer function:

import { layer } from "@effect/vitest"

class Database extends Context.Tag("Database")<Database, {
  query: (sql: string) => Effect.Effect<Array<unknown>>
}>() {
  static Test = Layer.succeed(Database, {
    query: (sql) => Effect.succeed([])
  })
}

layer(Database.Test)((it) => {
  it.effect("test 1", () =>
    Effect.gen(function* () {
      const db = yield* Database
      const results = yield* db.query("SELECT *")
      expect(results).toEqual([])
    })
  )

  it.effect("test 2", () =>
    Effect.gen(function* () {
      const db = yield* Database
      // Database available in all tests
    })
  )
})

// With name for describe block
layer(Database.Test)("Database tests", (it) => {
  it.effect("query test", () => /* ... */)
})

Nested Layers

Compose layers for complex dependencies:

layer(DatabaseLayer)((it) => {
  it.layer(UserServiceLayer)("user tests", (it) => {
    it.effect("has both dependencies", () =>
      Effect.gen(function* () {
        const db = yield* Database
        const userService = yield* UserService
        // Both available
      })
    )
  })
})

Excluding Test Services

Use live services instead of test services:

layer(MyServiceLayer, { excludeTestServices: true })((it) => {
  it.effect("uses real clock", () =>
    Effect.gen(function* () {
      // Uses actual Clock, not TestClock
    })
  )
})

Time-Dependent Testing with TestClock

Basic TestClock Usage

TestClock allows controlling time without waiting:

import { TestClock } from "effect"

it.effect("should handle delays", () =>
  Effect.gen(function* () {
    const fiber = yield* Effect.fork(
      Effect.sleep("5 seconds").pipe(Effect.as("done"))
    )

    // Advance time by 5 seconds instantly
    yield* TestClock.adjust("5 seconds")

    const result = yield* Fiber.join(fiber)
    expect(result).toBe("done")
  })
)

Testing Recurring Effects

Test periodic operations efficiently:

it.effect("should execute every minute", () =>
  Effect.gen(function* () {
    const queue = yield* Queue.unbounded<number>()

    // Fork effect that repeats every minute
    yield* Effect.fork(
      Queue.offer(queue, 1).pipe(
        Effect.delay("60 seconds"),
        Effect.forever
      )
    )

    // No effect before time passes
    const empty = yield* Queue.poll(queue)
    expect(Option.isNone(empty)).toBe(true)

    // Advance time
    yield* TestClock.adjust("60 seconds")

    // Effect executed once
    const value = yield* Queue.take(queue)
    expect(value).toBe(1)

    // Verify only one execution
    const stillEmpty = yield* Queue.poll(queue)
    expect(Option.isNone(stillEmpty)).toBe(true)
  })
)

Testing Clock Methods

it.effect("should track time correctly", () =>
  Effect.gen(function* () {
    const start = yield* Clock.currentTimeMillis

    yield* TestClock.adjust("1 minute")

    const end = yield* Clock.currentTimeMillis

    expect(end - start).toBeGreaterThanOrEqual(60_000)
  })
)

TestClock with Deferred

it.effect("should handle deferred with delays", () =>
  Effect.gen(function* () {
    const deferred = yield* Deferred.make<number, void>()

    yield* Effect.fork(
      Effect.sleep("10 seconds").pipe(
        Effect.zipRight(Deferred.succeed(deferred, 42))
      )
    )

    yield* TestClock.adjust("10 seconds")

    const result = yield* Deferred.await(deferred)
    expect(result).toBe(42)
  })
)

Error Testing

Testing Expected Failures

Use Effect.flip to convert failures to successes:

it.effect("should fail with error", () =>
  Effect.gen(function* () {
    const error = yield* Effect.flip(failingOperation())
    expect(error).toBeInstanceOf(UserNotFoundError)
    expect(error.userId).toBe("123")
  })
)

Testing with Exit

Use Effect.exit to capture both success and failure:

it.effect("should handle success", () =>
  Effect.gen(function* () {
    const exit = yield* Effect.exit(divide(4, 2))
    expect(exit).toEqual(Exit.succeed(2))
  })
)

it.effect("should handle failure", () =>
  Effect.gen(function* () {
    const exit = yield* Effect.exit(divide(4, 0))
    expect(exit).toEqual(Exit.fail("Cannot divide by zero"))
  })
)

Testing Error Types

it.effect("should fail with specific error", () =>
  Effect.gen(function* () {
    const exit = yield* Effect.exit(
      userService.getUser("nonexistent")
    )

    if (Exit.isFailure(exit)) {
      const cause = exit.cause
      expect(Cause.isFailType(cause)).toBe(true)
      const error = Cause.failureOrCause(cause)
      expect(error).toBeInstanceOf(NotFoundError)
    } else {
      throw new Error("Expected failure")
    }
  })
)

Property-Based Testing

Using it.prop for Pure Properties

import { FastCheck } from "effect"
import { it } from "@effect/vitest"

it.prop(
  "addition is commutative",
  [FastCheck.integer(), FastCheck.integer()],
  ([a, b]) => a + b === b + a
)

// With object syntax
it.prop(
  "multiplication distributes",
  { a: FastCheck.integer(), b: FastCheck.integer(), c: FastCheck.integer() },
  ({ a, b, c }) => a * (b + c) === a * b + a * c
)

Using it.effect.prop for Effect Properties

it.effect.prop(
  "database operations are idempotent",
  [FastCheck.string(), FastCheck.integer()],
  ([key, value]) =>
    Effect.gen(function* () {
      const db = yield* Database

      yield* db.set(key, value)
      const result1 = yield* db.get(key)

      yield* db.set(key, value)
      const result2 = yield* db.get(key)

      return result1 === result2
    })
)

With Schema Arbitraries

import { Schema } from "effect"

const User = Schema.Struct({
  id: Schema.String,
  age: Schema.Number.pipe(Schema.between(0, 120))
})

it.effect.prop(
  "user validation works",
  { user: User },
  ({ user }) =>
    Effect.gen(function* () {
      expect(user.age).toBeGreaterThanOrEqual(0)
      expect(user.age).toBeLessThanOrEqual(120)
      return true
    })
)

Configuring FastCheck

it.effect.prop(
  "property test",
  [FastCheck.integer()],
  ([n]) => Effect.succeed(n >= 0 || n < 0),
  {
    timeout: 10000,
    fastCheck: {
      numRuns: 1000,
      seed: 42,
      verbose: true
    }
  }
)

Test Control

Skipping Tests

it.effect.skip("not ready yet", () =>
  Effect.gen(function* () {
    // Will not run
  })
)

it.effect.skipIf(condition)("conditional skip", () =>
  Effect.gen(function* () {
    // Only runs if condition is false
  })
)

Running Single Tests

it.effect.only("debug this test", () =>
  Effect.gen(function* () {
    // Only this test runs
  })
)

Running Conditionally

it.effect.runIf(process.env.INTEGRATION_TESTS)("integration test", () =>
  Effect.gen(function* () {
    // Only runs if condition is true
  })
)

Expecting Failures

it.effect.fails("known failing test", () =>
  Effect.gen(function* () {
    // This test is expected to fail
    // Will pass if it fails, fail if it passes
    expect(1).toBe(2)
  })
)

Testing Flaky Operations

Use it.flakyTest for operations that may fail intermittently:

it.effect("retrying flaky operation", () =>
  it.flakyTest(
    Effect.gen(function* () {
      const random = yield* Random.nextBoolean
      if (random) {
        yield* Effect.fail("Random failure")
      }
    }),
    "5 seconds"  // Retry timeout
  )
)

Logging in Tests

Default Behavior (Suppressed)

it.effect("logs are suppressed", () =>
  Effect.gen(function* () {
    yield* Effect.log("This won't appear")
  })
)

Enabling Logs

import { Logger } from "effect"

it.effect("logs visible", () =>
  Effect.gen(function* () {
    yield* Effect.log("This will appear")
  }).pipe(Effect.provide(Logger.pretty))
)

// Or use it.live
it.live("logs visible", () =>
  Effect.gen(function* () {
    yield* Effect.log("This will appear")
  })
)

Testing Patterns

Arrange-Act-Assert Pattern

describe("UserService", () => {
  describe("getUser", () => {
    it.effect("should return user by id", () =>
      Effect.gen(function* () {
        // Arrange
        const userId = "user-123"
        const expectedUser = { id: userId, name: "Alice" }

        // Act
        const service = yield* UserService
        const user = yield* service.getUser(userId)

        // Assert
        expect(user).toEqual(expectedUser)
      }).pipe(Effect.provide(TestUserServiceLayer))
    )
  })
})

Testing STM Operations

it.effect("should handle concurrent updates", () =>
  Effect.gen(function* () {
    const counter = yield* TRef.make(0)

    const increment = STM.updateAndGet(counter, (n) => n + 1)

    yield* STM.commit(increment)
    yield* STM.commit(increment)

    const final = yield* STM.commit(TRef.get(counter))
    expect(final).toBe(2)
  })
)

Testing CRDT Operations

it.effect("should merge states correctly", () =>
  Effect.gen(function* () {
    const counter1 = yield* GCounter.make(ReplicaId("replica-1"))
    const counter2 = yield* GCounter.make(ReplicaId("replica-2"))

    yield* STM.commit(GCounter.increment(counter1, 10))
    yield* STM.commit(GCounter.increment(counter2, 20))

    const state2 = yield* STM.commit(GCounter.query(counter2))
    yield* STM.commit(GCounter.merge(counter1, state2))

    const result = yield* STM.commit(GCounter.value(counter1))
    expect(result).toBe(30)
  })
)

Testing Checklist

Before completing a testing task, verify:

  • Correct framework chosen (@effect/vitest vs vitest)
  • Test variant appropriate (effect/live/scoped/scopedLive)
  • Services provided via layers when needed
  • TestClock used for time-dependent operations
  • Errors tested with Effect.flip or Effect.exit
  • Edge cases covered
  • Property-based tests for general properties
  • Tests are deterministic (no real time, real random unless intended)
  • Test names describe behavior clearly
  • Resources properly scoped and cleaned up
  • All tests pass

Common Pitfalls

Don't Mix expect with assert

// L Wrong - mixing assertion libraries
import { assert } from "@effect/vitest"
it.effect("test", () =>
  Effect.gen(function* () {
    assert.strictEqual(result, expected)  // Don't use this
  })
)

//  Correct - use expect
import { expect } from "@effect/vitest"
it.effect("test", () =>
  Effect.gen(function* () {
    expect(result).toBe(expected)
  })
)

Don't Forget to Fork for TestClock

// L Wrong - will hang waiting for real time
it.effect("test", () =>
  Effect.gen(function* () {
    yield* Effect.sleep("5 seconds")  // Blocks!
    yield* TestClock.adjust("5 seconds")
  })
)

//  Correct - fork the effect
it.effect("test", () =>
  Effect.gen(function* () {
    const fiber = yield* Effect.fork(Effect.sleep("5 seconds"))
    yield* TestClock.adjust("5 seconds")
    yield* Fiber.join(fiber)
  })
)

Provide Layers to Effect, Not Test

// L Wrong - providing to wrong level
it.effect("test", () =>
  Effect.gen(function* () {
    const result = yield* someEffect
    expect(result).toBe(expected)
  })
).pipe(Effect.provide(layer))  // L Can't provide to test function

//  Correct - provide to Effect
it.effect("test", () =>
  Effect.gen(function* () {
    const result = yield* someEffect
    expect(result).toBe(expected)
  }).pipe(Effect.provide(layer))  //  Provide to Effect
)

Running Tests

# Run all tests
bun run test

# Watch mode
bun run test:watch

# Run specific file
bun run test path/to/file.test.ts

# Run with coverage
bun run test --coverage

Example: Complete Test Suite

import { describe, expect, it, layer } from "@effect/vitest"
import { Effect, Context, Layer, TestClock, Exit } from "effect"

// Service definition
class Counter extends Context.Tag("Counter")<Counter, {
  increment: () => Effect.Effect<void>
  value: () => Effect.Effect<number>
}>() {
  static Live = Layer.effect(
    Counter,
    Effect.gen(function* () {
      let count = 0
      return {
        increment: () => Effect.sync(() => { count++ }),
        value: () => Effect.succeed(count)
      }
    })
  )
}

// Tests
layer(Counter.Live)("Counter", (it) => {
  it.effect("should start at 0", () =>
    Effect.gen(function* () {
      const counter = yield* Counter
      const value = yield* counter.value()
      expect(value).toBe(0)
    })
  )

  it.effect("should increment", () =>
    Effect.gen(function* () {
      const counter = yield* Counter
      yield* counter.increment()
      const value = yield* counter.value()
      expect(value).toBe(1)
    })
  )

  it.effect("should handle multiple increments", () =>
    Effect.gen(function* () {
      const counter = yield* Counter
      yield* counter.increment()
      yield* counter.increment()
      yield* counter.increment()
      const value = yield* counter.value()
      expect(value).toBe(3)
    })
  )
})

This skill ensures comprehensive, reliable testing of Effect-based applications following best practices.