--- title: Test Effects with Services id: testing-with-services skillLevel: beginner applicationPatternId: testing summary: >- Learn how to test Effect programs that depend on services by providing test implementations. tags: - testing - services - dependency-injection - getting-started rule: description: Provide test implementations of services to make Effect programs testable. author: PaulJPhilp related: - testing-hello-world - testing-mock-dependencies lessonOrder: 1 --- ## Guideline When testing Effects that require services, provide test implementations using `Effect.provideService` or test layers. --- ## Rationale Effect's service pattern makes testing easy: 1. **Declare dependencies** - Effects specify what they need 2. **Inject test doubles** - Provide fake implementations for tests 3. **No mocking libraries** - Just provide different service implementations 4. **Type-safe** - Compiler ensures you provide all dependencies --- ## Good Example ```typescript import { describe, it, expect } from "vitest" import { Effect, Context } from "effect" // ============================================ // 1. Define a service // ============================================ class UserRepository extends Context.Tag("UserRepository")< UserRepository, { readonly findById: (id: string) => Effect.Effect readonly save: (user: User) => Effect.Effect } >() {} interface User { id: string name: string email: string } // ============================================ // 2. Code that uses the service // ============================================ const getUser = (id: string) => Effect.gen(function* () { const repo = yield* UserRepository const user = yield* repo.findById(id) if (!user) { return yield* Effect.fail(new Error(`User ${id} not found`)) } return user }) const createUser = (name: string, email: string) => Effect.gen(function* () { const repo = yield* UserRepository const user: User = { id: crypto.randomUUID(), name, email, } yield* repo.save(user) return user }) // ============================================ // 3. Create a test implementation // ============================================ const makeTestUserRepository = (initialUsers: User[] = []) => { const users = new Map(initialUsers.map(u => [u.id, u])) return UserRepository.of({ findById: (id) => Effect.succeed(users.get(id) ?? null), save: (user) => Effect.sync(() => { users.set(user.id, user) }), }) } // ============================================ // 4. Write tests // ============================================ describe("User Service Tests", () => { it("should find an existing user", async () => { const testUser: User = { id: "123", name: "Alice", email: "alice@example.com", } const testRepo = makeTestUserRepository([testUser]) const result = await Effect.runPromise( getUser("123").pipe( Effect.provideService(UserRepository, testRepo) ) ) expect(result).toEqual(testUser) }) it("should fail when user not found", async () => { const testRepo = makeTestUserRepository([]) await expect( Effect.runPromise( getUser("999").pipe( Effect.provideService(UserRepository, testRepo) ) ) ).rejects.toThrow("User 999 not found") }) it("should create and save a user", async () => { const savedUsers: User[] = [] const trackingRepo = UserRepository.of({ findById: () => Effect.succeed(null), save: (user) => Effect.sync(() => { savedUsers.push(user) }), }) const result = await Effect.runPromise( createUser("Bob", "bob@example.com").pipe( Effect.provideService(UserRepository, trackingRepo) ) ) expect(result.name).toBe("Bob") expect(result.email).toBe("bob@example.com") expect(savedUsers).toHaveLength(1) expect(savedUsers[0].name).toBe("Bob") }) }) ``` ## Test Double Strategies | Strategy | When to Use | |----------|-------------| | **In-memory store** | Testing data operations | | **Tracking wrapper** | Verify methods were called | | **Stub returns** | Fixed responses for specific scenarios | | **Failing service** | Test error handling paths | ## Key Points 1. **Services are interfaces** - Easy to provide alternative implementations 2. **No global mocks** - Each test provides its own dependencies 3. **Type-safe** - Compiler ensures test doubles match service interface 4. **Isolated tests** - Each test has its own service instance