effect-testing

Master testing Effect applications with test utilities, mock layers, and patterns for testing effectful code. This skill covers unit testing, integration testing, and testing concurrent and resource-managed code.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "effect-testing" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-effect-testing

Effect Testing

Master testing Effect applications with test utilities, mock layers, and patterns for testing effectful code. This skill covers unit testing, integration testing, and testing concurrent and resource-managed code.

Basic Effect Testing

Testing with Effect.gen

import { Effect } from "effect" import { describe, it, expect } from "vitest"

describe("User Service", () => { it("should fetch user by ID", async () => { const program = Effect.gen(function* () { const user = yield* fetchUser("123") return user })

const result = await Effect.runPromise(program.pipe(
  Effect.provide(TestLayer)
))

expect(result.id).toBe("123")
expect(result.name).toBe("Alice")

}) })

Testing Success and Failure

import { Effect, Exit } from "effect" import { describe, it, expect } from "vitest"

describe("Validation", () => { it("should succeed with valid email", async () => { const program = validateEmail("alice@example.com")

const result = await Effect.runPromise(program)

expect(result).toBe("alice@example.com")

})

it("should fail with invalid email", async () => { const program = validateEmail("invalid")

const exit = await Effect.runPromiseExit(program)

expect(Exit.isFailure(exit)).toBe(true)

if (Exit.isFailure(exit)) {
  const error = Cause.failureOption(exit.cause)
  expect(error._tag).toBe("ValidationError")
}

}) })

Mock Layers for Testing

Creating Test Layers

import { Context, Effect, Layer } from "effect"

interface UserRepository { findById: (id: string) => Effect.Effect<Option<User>, DbError, never> save: (user: User) => Effect.Effect<User, DbError, never> }

const UserRepository = Context.GenericTag<UserRepository>("UserRepository")

// In-memory test implementation const UserRepositoryTest = Layer.succeed( UserRepository, { findById: (id: string) => Effect.succeed( id === "1" ? Option.some({ id: "1", name: "Alice", email: "alice@example.com" }) : Option.none() ),

save: (user: User) =>
  Effect.succeed(user)

} )

// Use in tests const testProgram = Effect.gen(function* () { const repo = yield* UserRepository const user = yield* repo.findById("1") return user }).pipe( Effect.provide(UserRepositoryTest) )

Stateful Mock Layers

import { Context, Effect, Layer, Ref } from "effect"

// Mock with state const UserRepositoryStateful = Layer.effect( UserRepository, Effect.gen(function* () { const storage = yield* Ref.make<Map<string, User>>(new Map([ ["1", { id: "1", name: "Alice", email: "alice@example.com" }] ]))

return {
  findById: (id: string) =>
    storage.get.pipe(
      Effect.map((map) => {
        const user = map.get(id)
        return user ? Option.some(user) : Option.none()
      })
    ),

  save: (user: User) =>
    storage.update((map) => map.set(user.id, user)).pipe(
      Effect.map(() => user)
    )
}

}) )

// Test with state describe("User Repository", () => { it("should save and retrieve user", async () => { const program = Effect.gen(function* () { const repo = yield* UserRepository

  const newUser = { id: "2", name: "Bob", email: "bob@example.com" }
  yield* repo.save(newUser)

  const retrieved = yield* repo.findById("2")
  return retrieved
}).pipe(
  Effect.provide(UserRepositoryStateful)
)

const result = await Effect.runPromise(program)

expect(Option.isSome(result)).toBe(true)
if (Option.isSome(result)) {
  expect(result.value.name).toBe("Bob")
}

}) })

Spy Layers

Recording Calls

import { Context, Effect, Layer, Ref } from "effect"

interface LoggerCalls { info: string[] error: string[] }

const LoggerSpy = Layer.effect( Logger, Effect.gen(function* () { const calls = yield* Ref.make<LoggerCalls>({ info: [], error: [] })

return {
  logger: {
    info: (message: string) =>
      calls.update((c) => ({
        ...c,
        info: [...c.info, message]
      })),

    error: (message: string) =>
      calls.update((c) => ({
        ...c,
        error: [...c.error, message]
      }))
  },

  getCalls: () => calls.get
}

}) )

// Test with spy describe("User Service", () => { it("should log user creation", async () => { const program = Effect.gen(function* () { const spy = yield* LoggerSpy const service = yield* UserService

  yield* service.createUser({ name: "Alice" })

  const calls = yield* spy.getCalls()
  return calls
}).pipe(
  Effect.provide(Layer.merge(LoggerSpy, UserServiceLive))
)

const calls = await Effect.runPromise(program)

expect(calls.info).toContain("Creating user: Alice")

}) })

Testing Error Scenarios

Testing Expected Errors

import { Effect } from "effect" import { describe, it, expect } from "vitest"

describe("Error Handling", () => { it("should handle NotFoundError", async () => { const program = Effect.gen(function* () { const result = yield* fetchUser("999").pipe( Effect.catchTag("NotFoundError", (error) => Effect.succeed({ id: "default", name: "Guest" }) ) ) return result })

const result = await Effect.runPromise(program.pipe(
  Effect.provide(TestLayer)
))

expect(result.name).toBe("Guest")

})

it("should propagate unhandled errors", async () => { const program = Effect.gen(function* () { const result = yield* fetchUser("999") return result })

await expect(
  Effect.runPromise(program.pipe(
    Effect.provide(TestLayer)
  ))
).rejects.toThrow()

}) })

Testing Error Recovery

import { Effect } from "effect" import { describe, it, expect } from "vitest"

describe("Retry Logic", () => { it("should retry on network error", async () => { let attempts = 0

const unstableOperation = Effect.gen(function* () {
  attempts++
  if (attempts &#x3C; 3) {
    return yield* Effect.fail({ _tag: "NetworkError" })
  }
  return yield* Effect.succeed("Success")
})

const program = unstableOperation.pipe(
  Effect.retry(Schedule.recurs(5))
)

const result = await Effect.runPromise(program)

expect(result).toBe("Success")
expect(attempts).toBe(3)

}) })

Testing Concurrent Code

Testing Parallel Execution

import { Effect, Ref } from "effect" import { describe, it, expect } from "vitest"

describe("Concurrent Operations", () => { it("should process items in parallel", async () => { const program = Effect.gen(function* () { const processed = yield* Ref.make<string[]>([])

  const items = ["a", "b", "c", "d", "e"]

  yield* Effect.all(
    items.map((item) =>
      Effect.gen(function* () {
        yield* Effect.sleep("10 millis")
        yield* processed.update((p) => [...p, item])
      })
    ),
    { concurrency: "unbounded" }
  )

  return yield* processed.get
})

const result = await Effect.runPromise(program)

expect(result).toHaveLength(5)
expect(result).toContain("a")
expect(result).toContain("b")

}) })

Testing Fiber Interruption

import { Effect, Fiber, Ref } from "effect" import { describe, it, expect } from "vitest"

describe("Interruption", () => { it("should interrupt long-running task", async () => { const program = Effect.gen(function* () { const completed = yield* Ref.make(false)

  const fiber = yield* Effect.fork(
    Effect.gen(function* () {
      yield* Effect.sleep("1 second")
      yield* completed.set(true)
    })
  )

  yield* Effect.sleep("100 millis")
  yield* Fiber.interrupt(fiber)

  return yield* completed.get
})

const result = await Effect.runPromise(program)

expect(result).toBe(false)

}) })

Testing Resource Management

Testing Cleanup

import { Effect, Ref } from "effect" import { describe, it, expect } from "vitest"

describe("Resource Management", () => { it("should clean up resources on success", async () => { const program = Effect.gen(function* () { const cleaned = yield* Ref.make(false)

  yield* Effect.scoped(
    Effect.gen(function* () {
      yield* Effect.addFinalizer(() =>
        cleaned.set(true)
      )

      yield* Effect.succeed("done")
    })
  )

  return yield* cleaned.get
})

const result = await Effect.runPromise(program)

expect(result).toBe(true)

})

it("should clean up resources on failure", async () => { const program = Effect.gen(function* () { const cleaned = yield* Ref.make(false)

  const result = yield* Effect.scoped(
    Effect.gen(function* () {
      yield* Effect.addFinalizer(() =>
        cleaned.set(true)
      )

      yield* Effect.fail({ _tag: "TestError" })
    })
  ).pipe(
    Effect.catchAll(() => Effect.succeed("handled"))
  )

  const wasCleanedUp = yield* cleaned.get

  return { result, wasCleanedUp }
})

const { result, wasCleanedUp } = await Effect.runPromise(program)

expect(result).toBe("handled")
expect(wasCleanedUp).toBe(true)

}) })

Property-Based Testing

Using fast-check with Effect

import { Effect } from "effect" import { describe, it } from "vitest" import * as fc from "fast-check"

describe("Property Tests", () => { it("should always succeed for valid emails", () => { fc.assert( fc.asyncProperty( fc.emailAddress(), async (email) => { const program = validateEmail(email)

      const result = await Effect.runPromise(program)

      expect(result).toBe(email.toLowerCase())
    }
  )
)

})

it("should handle any string input", () => { fc.assert( fc.asyncProperty( fc.string(), async (input) => { const program = parseJSON(input).pipe( Effect.catchAll(() => Effect.succeed(null)) )

      const result = await Effect.runPromise(program)

      // Should never throw
      expect(result).toBeDefined()
    }
  )
)

}) })

Testing Best Practices

Test Organization

import { Effect, Layer } from "effect" import { describe, it, beforeEach, expect } from "vitest"

describe("User Service", () => { // Shared test layer const TestLayer = Layer.merge( UserRepositoryTest, LoggerTest, ConfigTest )

describe("createUser", () => { it("should create user with valid data", async () => { const program = Effect.gen(function* () { const service = yield* UserService const user = yield* service.createUser({ name: "Alice", email: "alice@example.com" }) return user }).pipe( Effect.provide(TestLayer) )

  const result = await Effect.runPromise(program)

  expect(result.name).toBe("Alice")
})

it("should fail with invalid email", async () => {
  const program = Effect.gen(function* () {
    const service = yield* UserService
    const user = yield* service.createUser({
      name: "Bob",
      email: "invalid"
    })
    return user
  }).pipe(
    Effect.provide(TestLayer)
  )

  await expect(Effect.runPromise(program)).rejects.toThrow()
})

}) })

Best Practices

Use Test Layers: Create dedicated test implementations for services.

Test Error Paths: Test both success and failure scenarios.

Mock Dependencies: Use layers to inject test dependencies.

Test Concurrency: Verify concurrent behavior with multiple fibers.

Test Cleanup: Ensure resources are cleaned up properly.

Use Property Tests: Test invariants with property-based testing.

Isolate Tests: Each test should be independent.

Test Interruption: Verify correct behavior on interruption.

Use Spies: Track calls to verify behavior.

Test Edge Cases: Cover boundary conditions and error cases.

Common Pitfalls

Not Providing Layers: Forgetting to provide required services.

Shared State: Tests interfering with each other via shared state.

Not Testing Errors: Only testing happy paths.

Missing Cleanup Tests: Not verifying finalizers execute.

Ignoring Concurrency: Not testing concurrent behavior.

Flaky Tests: Race conditions in concurrent tests.

Over-Mocking: Mocking too much, losing integration value.

Not Testing Interruption: Missing interruption scenarios.

Hardcoded Timing: Tests that depend on specific timing.

Missing Exit Checks: Not verifying Exit values properly.

When to Use This Skill

Use effect-testing when you need to:

  • Write unit tests for Effect code

  • Create integration tests with dependencies

  • Test error handling and recovery

  • Verify concurrent behavior

  • Test resource cleanup

  • Mock external services

  • Verify retry logic

  • Test interruption handling

  • Use property-based testing

  • Build reliable test suites

Resources

Official Documentation

  • Effect Testing

  • Testing Guide

Testing Libraries

  • Vitest

  • Jest

  • fast-check

Related Skills

  • effect-core-patterns - Basic Effect operations

  • effect-dependency-injection - Creating test layers

  • effect-error-handling - Testing error scenarios

  • effect-concurrency - Testing concurrent code

  • effect-resource-management - Testing cleanup

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

typescript-type-system

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

c-systems-programming

No summary provided by upstream source.

Repository SourceNeeds Review