| name | effect-testing |
| user-invocable | false |
| description | Use when testing Effect code including Effect.gen in tests, test layers, mocking services, and testing error scenarios. Use for writing tests for Effect applications. |
| allowed-tools | ["Bash","Read","Write","Edit"] |
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()
}
})
})
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")
const UserRepositoryTest = Layer.succeed(
UserRepository,
{
findById: (id: string) =>
Effect.succeed(
id === "1"
? Option.some({ id: "1", name: , : })
: .()
),
:
.(user)
}
)
testProgram = .(* () {
repo = *
user = * repo.()
user
}).(
.()
)
Stateful Mock Layers
import { Context, Effect, Layer, Ref } from "effect"
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: () =>
storage.( map.(user., user)).(
.( user)
)
}
})
)
(, {
(, () => {
program = .(* () {
repo = *
newUser = { : , : , : }
* repo.(newUser)
retrieved = * repo.()
retrieved
}).(
.()
)
result = .(program)
(.(result)).()
(.(result)) {
(result..).()
}
})
})
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.
}
})
)
(, {
(, () => {
program = .(* () {
spy = *
service = *
* service.({ : })
calls = * spy.()
calls
}).(
.(.(, ))
)
calls = .(program)
(calls.).()
})
})
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 = .(* () {
result = * ()
result
})
(
.(program.(
.()
))
)..()
})
})
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 < 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
})
result = .(program)
(result).()
(result).()
(result).()
})
})
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.(program)
(result).()
})
})
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(, () => {
program = .(* () {
cleaned = * .()
result = * .(
.(* () {
* .(
cleaned.()
)
* .({ : })
})
).(
.( .())
)
wasCleanedUp = * cleaned.
{ result, wasCleanedUp }
})
{ result, wasCleanedUp } = .(program)
(result).()
(wasCleanedUp).()
})
})
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 = .(program)
(result).()
}
)
)
})
})
Testing Best Practices
Test Organization
import { Effect, Layer } from "effect"
import { describe, it, beforeEach, expect } from "vitest"
describe("User Service", () => {
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.).()
})
(, () => {
program = .(* () {
service = *
user = * service.({
: ,
:
})
user
}).(
.()
)
(.(program))..()
})
})
})
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
Testing Libraries
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