Agent skill
effect-testing
Comprehensive testing patterns for Effect-TS services, errors, layers, and effects. Use this skill when writing tests for Effect-based code.
Install this agent skill to your Project
npx add-skill https://github.com/enitrat/skill-issue/tree/main/plugins/personal-skills/skills/effect-testing
SKILL.md
Effect-TS Testing Patterns
Comprehensive testing patterns for Effect-TS services, errors, layers, and effects. Use this skill when writing tests for Effect-based code.
Core Testing Setup
@effect/vitest Integration
import { describe, it, expect } from "@effect/vitest"
import { Effect } from "effect"
// Basic test - it.effect provides TestContext automatically
it.effect("test name", () =>
Effect.gen(function* () {
const result = yield* someEffect
expect(result).toBe(expected)
})
)
// Test with layers - provide dependencies to all tests
it.layer(MyServiceLive)("test with service", () =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* service.doSomething()
expect(result).toBe(expected)
})
)
// Scoped tests - automatically handles resource cleanup
it.scoped("test with resources", () =>
Effect.gen(function* () {
const resource = yield* acquireResource
// resource automatically released after test
yield* useResource(resource)
})
)
Key Features:
it.effect- automatic TestContext provision (TestClock, TestRandom, etc.)it.layer- provide layers to test suite, shared across testsit.scoped- automatic resource cleanup- Full fiber dumps with causes, spans, and logs for better errors
Testing Services
Mock Service with Layer.succeed
import { Effect, Context, Layer } from "effect"
// Service definition
class DatabaseService extends Context.Tag("DatabaseService")<
DatabaseService,
{
readonly query: (sql: string) => Effect.Effect<unknown>
}
>() {}
// Live implementation (production)
export const DatabaseServiceLive = Layer.succeed(
DatabaseService,
{
query: (sql) => Effect.promise(() => realDatabase.query(sql))
}
)
// Test implementation (mocked)
export const DatabaseServiceTest = Layer.succeed(
DatabaseService,
{
query: (sql) => Effect.succeed({ rows: [{ id: 1, name: "test" }] })
}
)
// Usage in test
it.layer(DatabaseServiceTest)("queries database", () =>
Effect.gen(function* () {
const db = yield* DatabaseService
const result = yield* db.query("SELECT * FROM users")
expect(result).toEqual({ rows: [{ id: 1, name: "test" }] })
})
)
Convention: Use "Live" suffix for production, "Test" suffix for mocks.
Mock Service with Layer.mock
import { Layer } from "effect"
// Partial mock - only implement methods you need
const PartialDatabaseMock = Layer.mock(DatabaseService, {
query: (sql) => Effect.succeed({ rows: [] })
// Other methods throw UnimplementedError when called
})
// Full mock with test doubles
const MockWithSpy = Layer.succeed(DatabaseService, {
query: vi.fn().mockReturnValue(Effect.succeed({ rows: [] }))
})
Testing Services with Dependencies
class UserService extends Context.Tag("UserService")<
UserService,
{
readonly getUser: (id: number) => Effect.Effect<User, UserNotFound>
}
>() {}
class EmailService extends Context.Tag("EmailService")<
EmailService,
{
readonly sendEmail: (to: string, body: string) => Effect.Effect<void>
}
>() {}
// Service that depends on other services
class NotificationService extends Context.Tag("NotificationService")<
NotificationService,
{
readonly notifyUser: (userId: number) => Effect.Effect<void, UserNotFound>
}
>() {
static Live = Layer.effect(
NotificationService,
Effect.gen(function* () {
const users = yield* UserService
const email = yield* EmailService
return {
notifyUser: (userId) =>
Effect.gen(function* () {
const user = yield* users.getUser(userId)
yield* email.sendEmail(user.email, "Notification")
})
}
})
)
}
// Test with all dependencies mocked
const TestLayer = Layer.mergeAll(
UserServiceTest,
EmailServiceTest
).pipe(Layer.provideMerge(NotificationService.Live))
it.layer(TestLayer)("sends notification", () =>
Effect.gen(function* () {
const notif = yield* NotificationService
yield* notif.notifyUser(1)
// Verify email was sent using mocked EmailService
})
)
Testing Errors
Expected Error Testing
import { Effect, Exit } from "effect"
class MyError extends Data.TaggedError("MyError")<{
readonly message: string
}> {}
it.effect("handles expected errors", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(
Effect.fail(new MyError({ message: "test error" }))
)
// Check error occurred
expect(Exit.isFailure(result)).toBe(true)
// Check error type
if (Exit.isFailure(result)) {
const cause = result.cause
expect(Cause.isFailType(cause)).toBe(true)
// Extract error value
const error = Cause.failureOption(cause)
expect(error).toBeSome()
expect(Option.getOrThrow(error)).toBeInstanceOf(MyError)
}
})
)
// Alternative: use catchTag to verify error
it.effect("catches specific error type", () =>
Effect.gen(function* () {
let caught = false
yield* effectThatFails.pipe(
Effect.catchTag("MyError", (error) =>
Effect.sync(() => {
caught = true
expect(error.message).toBe("test error")
})
)
)
expect(caught).toBe(true)
})
)
Multiple Error Types
type UserServiceError = UserNotFound | DatabaseError | ValidationError
it.effect("handles multiple error types", () =>
Effect.gen(function* () {
const result = yield* Effect.either(service.getUser(999))
expect(Either.isLeft(result)).toBe(true)
if (Either.isLeft(result)) {
// Pattern match on error type
const error = result.left
if (error._tag === "UserNotFound") {
expect(error.userId).toBe(999)
}
}
})
)
Defect Testing (Unexpected Errors)
it.effect("handles unexpected errors (defects)", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(
Effect.die(new Error("Unexpected"))
)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
expect(Cause.isDie(result.cause)).toBe(true)
}
})
)
Testing with TestClock
import { TestClock } from "effect"
it.effect("delays execution", () =>
Effect.gen(function* () {
let executed = false
// Fork effect that delays 1 second
const fiber = yield* Effect.fork(
Effect.delay("1 second")(
Effect.sync(() => { executed = true })
)
)
// Verify not executed yet
expect(executed).toBe(false)
// Advance time by 1 second
yield* TestClock.adjust("1 second")
// Wait for fiber to complete
yield* Fiber.join(fiber)
// Verify executed after time advance
expect(executed).toBe(true)
})
)
// Testing intervals
it.effect("processes scheduled tasks", () =>
Effect.gen(function* () {
const results: number[] = []
const fiber = yield* Effect.fork(
Effect.repeat(
Effect.sync(() => results.push(Date.now())),
Schedule.spaced("100 millis")
).pipe(Effect.timeout("1 second"))
)
// Advance time in increments
yield* TestClock.adjust("100 millis")
yield* TestClock.adjust("100 millis")
yield* TestClock.adjust("100 millis")
yield* Fiber.join(fiber)
expect(results.length).toBeGreaterThan(0)
})
)
Testing with TestRandom
import { TestRandom } from "effect"
it.effect("deterministic random values", () =>
Effect.gen(function* () {
// Set fixed random seed for reproducibility
yield* TestRandom.setSeed(42)
const random1 = yield* Random.next
const random2 = yield* Random.next
// Reset seed - same values again
yield* TestRandom.setSeed(42)
const random3 = yield* Random.next
expect(random3).toBe(random1)
})
)
Testing Layers
Fresh Layers Per Test
// Helper to create fresh layer for each test
const makeFreshLayer = () =>
Layer.succeed(MyService, {
state: { counter: 0 }, // Fresh state per test
increment: function() {
this.state.counter++
return Effect.succeed(this.state.counter)
}
})
describe("isolated tests", () => {
it.effect("test 1", () =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* service.increment()
expect(result).toBe(1) // Fresh state
}).pipe(Effect.provide(makeFreshLayer()))
)
it.effect("test 2", () =>
Effect.gen(function* () {
const service = yield* MyService
const result = yield* service.increment()
expect(result).toBe(1) // Fresh state again
}).pipe(Effect.provide(makeFreshLayer()))
)
})
Layer Composition Testing
// Test layer dependencies are wired correctly
it.effect("layer composition", () =>
Effect.gen(function* () {
// This test verifies all dependencies resolve
const service = yield* TopLevelService
const result = yield* service.operation()
expect(result).toBeDefined()
}).pipe(
Effect.provide(
TopLevelService.Live.pipe(
Layer.provide(MiddleService.Live),
Layer.provide(BottomService.Live)
)
)
)
)
Testing ConfigProvider
import { ConfigProvider, Layer } from "effect"
// Mock config for tests
const TestConfig = Layer.setConfigProvider(
ConfigProvider.fromMap(
new Map([
["API_KEY", "test-key"],
["DATABASE_URL", "postgres://test"],
["PORT", "3000"]
])
)
)
it.layer(TestConfig)("uses test configuration", () =>
Effect.gen(function* () {
const apiKey = yield* Config.string("API_KEY")
expect(apiKey).toBe("test-key")
})
)
Testing HTTP Clients
import { FetchHttpClient, HttpClient } from "@effect/platform"
// Mock fetch for testing
const MockFetch = Layer.succeed(
FetchHttpClient.Fetch,
() => Promise.resolve(
new Response(JSON.stringify({ data: "test" }), {
status: 200,
headers: { "content-type": "application/json" }
})
)
)
it.layer(MockFetch)("makes HTTP request", () =>
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient
const response = yield* client.get("https://api.example.com/data")
const json = yield* response.json
expect(json).toEqual({ data: "test" })
})
)
Testing Concurrent Effects
it.effect("race condition handling", () =>
Effect.gen(function* () {
const ref = yield* Ref.make(0)
// Run 100 concurrent increments
yield* Effect.all(
Array.from({ length: 100 }, () =>
Ref.update(ref, (n) => n + 1)
),
{ concurrency: "unbounded" }
)
const final = yield* Ref.get(ref)
expect(final).toBe(100) // Ref is atomic
})
)
it.effect("parallel execution", () =>
Effect.gen(function* () {
const fiber1 = yield* Effect.fork(longRunningTask1)
const fiber2 = yield* Effect.fork(longRunningTask2)
const [result1, result2] = yield* Effect.all([
Fiber.join(fiber1),
Fiber.join(fiber2)
])
expect(result1).toBeDefined()
expect(result2).toBeDefined()
})
)
Testing Streams
import { Stream, Chunk } from "effect"
it.effect("stream processing", () =>
Effect.gen(function* () {
const stream = Stream.range(1, 5)
const result = yield* Stream.runCollect(
stream.pipe(Stream.map((n) => n * 2))
)
expect(Chunk.toArray(result)).toEqual([2, 4, 6, 8, 10])
})
)
it.effect("stream error handling", () =>
Effect.gen(function* () {
const stream = Stream.range(1, 10).pipe(
Stream.map((n) =>
n === 5 ? Effect.fail("error") : Effect.succeed(n)
),
Stream.flatMap(identity)
)
const result = yield* Effect.exit(Stream.runCollect(stream))
expect(Exit.isFailure(result)).toBe(true)
})
)
Testing Effect.runPromise
// For integrating with Promise-based test frameworks
describe("promise integration", () => {
it("converts effect to promise", async () => {
const effect = Effect.succeed(42)
const result = await Effect.runPromise(effect)
expect(result).toBe(42)
})
it("rejects on failure", async () => {
const effect = Effect.fail(new MyError({ message: "fail" }))
await expect(Effect.runPromise(effect)).rejects.toThrow()
})
it("provides dependencies for promise", async () => {
const effect = Effect.gen(function* () {
const service = yield* MyService
return yield* service.doSomething()
})
const result = await Effect.runPromise(
effect.pipe(Effect.provide(MyServiceTest))
)
expect(result).toBeDefined()
})
})
Best Practices
1. Use Descriptive Test Names
// Good
it.effect("returns user when found in database", () => ...)
it.effect("fails with UserNotFound when user does not exist", () => ...)
// Bad
it.effect("test 1", () => ...)
it.effect("works", () => ...)
2. Test Happy Path and Error Cases
describe("UserService.getUser", () => {
it.effect("returns user when exists", () => ...)
it.effect("fails with UserNotFound when not exists", () => ...)
it.effect("fails with DatabaseError on connection failure", () => ...)
})
3. Keep Tests Isolated
// Good - each test gets fresh state
it.effect("test 1", () =>
Effect.provide(effect, makeFreshLayer())
)
// Bad - shared state between tests
const sharedLayer = makeLayer()
it.layer(sharedLayer)("test 1", () => ...)
it.layer(sharedLayer)("test 2", () => ...) // May see state from test 1
4. Use TestClock for Time-Based Tests
// Good - instant, deterministic
it.effect("delays 1 hour", () =>
Effect.gen(function* () {
const fiber = yield* Effect.fork(Effect.delay("1 hour")(task))
yield* TestClock.adjust("1 hour")
yield* Fiber.join(fiber)
})
)
// Bad - actually waits 1 hour
it.effect("delays 1 hour", () =>
Effect.delay("1 hour")(task)
)
5. Mock External Dependencies
// Good - all external services mocked
const TestLayer = Layer.mergeAll(
DatabaseServiceTest,
EmailServiceTest,
PaymentServiceTest
)
// Bad - tests hit real services (slow, flaky, expensive)
const TestLayer = Layer.mergeAll(
DatabaseServiceLive, // ❌ Real DB
EmailServiceLive, // ❌ Sends real emails
PaymentServiceLive // ❌ Charges real money
)
6. Test Error Propagation
it.effect("propagates errors through effect chain", () =>
Effect.gen(function* () {
const result = yield* Effect.exit(
service.getUser(999).pipe(
Effect.flatMap((user) => service.processUser(user)),
Effect.flatMap((processed) => service.saveUser(processed))
)
)
// Verify UserNotFound from getUser propagated through chain
expect(Exit.isFailure(result)).toBe(true)
})
)
7. Use it.scoped for Resource Management
// Good - automatic cleanup
it.scoped("uses database connection", () =>
Effect.gen(function* () {
const conn = yield* acquireConnection // Scope manages lifecycle
yield* useConnection(conn)
// Connection automatically closed after test
})
)
// Bad - manual cleanup (easy to forget)
it.effect("uses database connection", () =>
Effect.gen(function* () {
const conn = yield* acquireConnection
try {
yield* useConnection(conn)
} finally {
yield* releaseConnection(conn)
}
})
)
Common Patterns
Testing Retry Logic
it.effect("retries on failure", () =>
Effect.gen(function* () {
let attempts = 0
const effect = Effect.gen(function* () {
attempts++
if (attempts < 3) {
yield* Effect.fail("retry me")
}
return "success"
}).pipe(
Effect.retry(Schedule.recurs(5))
)
const result = yield* effect
expect(result).toBe("success")
expect(attempts).toBe(3)
})
)
Testing Timeouts
it.effect("times out long operations", () =>
Effect.gen(function* () {
const longOp = Effect.delay("5 seconds")(Effect.succeed("done"))
const result = yield* Effect.exit(
longOp.pipe(Effect.timeout("1 second"))
)
expect(Exit.isFailure(result)).toBe(true)
// Verify it's a TimeoutException
}).pipe(Effect.provide(TestClock.layer))
)
Testing Interruption
it.effect("handles interruption gracefully", () =>
Effect.gen(function* () {
const ref = yield* Ref.make("initial")
const fiber = yield* Effect.fork(
Effect.gen(function* () {
yield* Ref.set(ref, "started")
yield* Effect.sleep("1 hour")
yield* Ref.set(ref, "completed") // Should never reach here
})
)
yield* TestClock.adjust("1 second")
yield* Fiber.interrupt(fiber)
const value = yield* Ref.get(ref)
expect(value).toBe("started") // Not "completed"
})
)
Sources
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
tdd
Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development.
sdk-documentation
Rules and patterns for writing comprehensive, high-quality SDK documentation for public libraries. Covers documentation architecture, narrative tone, user guides, and API references. Use when: (1) Writing or reviewing documentation for a public SDK/library, (2) Creating API reference pages for hooks/functions/classes, (3) Writing getting-started guides or tutorials, (4) Structuring a documentation site from scratch, (5) Reviewing documentation quality and consistency, (6) Setting up a VitePress or GitBook documentation site for an SDK.
super-ralph
Build multi-phase AI development pipelines with the Smithers workflow engine (v0.8.2). Use when: (1) Setting up a SuperRalph workflow for a repo (focuses, focusDirs, focusTestSuites, agents) (2) Debugging a run (ticket explosion, duplicate tickets, stalled nodes) (3) Understanding the pipeline phases and what generates tickets (4) Avoiding common configuration mistakes that cause runaway ticket counts
prd-authoring
tanstack-best-practices
Best practices for building hook libraries with TanStack Query. Use when: (1) Writing useQuery/useMutation hooks that wrap async data-fetching functions, (2) Designing query key schemas and cache identity systems, (3) Building framework-agnostic query options factories, (4) Implementing cache invalidation patterns (invalidate vs remove vs setQueryData), (5) Wrapping TanStack Query in a multi-layered library (core actions to query options to framework hooks), (6) Handling non-serializable values (bigint, class instances) in query keys, (7) Bridging external stores (zustand, signals) with TanStack Query reactivity. Derived from wagmi's production architecture (React/Vue/Solid Ethereum hooks).
smithers
Build multi-phase AI development pipelines with the Smithers workflow engine (v0.8.2). Use when: (1) Initializing a new Smithers project in a target directory (use the init CLI) (2) Adding phases or steps to existing workflows (3) Implementing review loops, pass tracking, or phase gating (4) Debugging workflow orchestration issues (Ralph loops, ctx.output, data threading) (5) Writing instruction MDX files for project configs (6) Configuring agents or per-role overrides
Didn't find tool you were looking for?