Skip to content

Latest commit

 

History

History
127 lines (103 loc) · 4.57 KB

File metadata and controls

127 lines (103 loc) · 4.57 KB
title Mocking Dependencies in Tests
id mocking-dependencies-in-tests
skillLevel intermediate
applicationPatternId testing
summary Use a test-specific Layer to provide mock implementations of services your code depends on, enabling isolated and deterministic unit tests.
tags
testing
mocking
dependency-injection
layer
service
unit-test
rule
description
Provide mock service implementations via a test-specific Layer to isolate the unit under test.
related
model-dependencies-as-services
define-service-with-effect-service
create-a-testable-http-client-service
author effect_website
lessonOrder 2

Guideline

To test a piece of code in isolation, identify its service dependencies and provide mock implementations for them using a test-specific Layer. The most common way to create a mock layer is with Layer.succeed(ServiceTag, mockImplementation).


Rationale

The primary goal of a unit test is to verify the logic of a single unit of code, independent of its external dependencies. Effect's dependency injection system is designed to make this easy and type-safe.

By providing a mock Layer in your test, you replace a real dependency (like an HttpClient that makes network calls) with a fake one that returns predictable data. This provides several key benefits:

  • Determinism: Your tests always produce the same result, free from the flakiness of network or database connections.
  • Speed: Tests run instantly without waiting for slow I/O operations.
  • Type Safety: The TypeScript compiler ensures your mock implementation perfectly matches the real service's interface, preventing your tests from becoming outdated.
  • Explicitness: The test setup clearly documents all the dependencies required for the code to run.

Good Example

We want to test a Notifier service that uses an EmailClient to send emails. In our test, we provide a mock EmailClient that doesn't actually send emails but just returns a success value.

import { Effect, Layer } from "effect";

// --- The Services ---
interface EmailClientService {
  send: (address: string, body: string) => Effect.Effect<void>;
}

class EmailClient extends Effect.Service<EmailClientService>()("EmailClient", {
  sync: () => ({
    send: (address: string, body: string) =>
      Effect.sync(() => Effect.log(`Sending email to ${address}: ${body}`)),
  }),
}) {}

interface NotifierService {
  notifyUser: (userId: number, message: string) => Effect.Effect<void>;
}

class Notifier extends Effect.Service<NotifierService>()("Notifier", {
  effect: Effect.gen(function* () {
    const emailClient = yield* EmailClient;
    return {
      notifyUser: (userId: number, message: string) =>
        emailClient.send(`user-${userId}@example.com`, message),
    };
  }),
  dependencies: [EmailClient.Default],
}) {}

// Create a program that uses the Notifier service
const program = Effect.gen(function* () {
  yield* Effect.log("Using default EmailClient implementation...");
  const notifier = yield* Notifier;
  yield* notifier.notifyUser(123, "Your invoice is ready.");

  // Create mock EmailClient that logs differently
  yield* Effect.log("\nUsing mock EmailClient implementation...");
  const mockEmailClient = Layer.succeed(EmailClient, {
    send: (address: string, body: string) =>
      // Directly return the Effect.log without nesting it in Effect.sync
      Effect.log(`MOCK: Would send to ${address} with body: ${body}`),
  } as EmailClientService);

  // Run the same notification with mock client
  yield* Effect.gen(function* () {
    const notifier = yield* Notifier;
    yield* notifier.notifyUser(123, "Your invoice is ready.");
  }).pipe(Effect.provide(mockEmailClient));
});

// Run the program
Effect.runPromise(Effect.provide(program, Notifier.Default));

Anti-Pattern

Testing your business logic using the "live" implementation of its dependencies. This creates an integration test, not a unit test. It will be slow, unreliable, and may have real-world side effects (like actually sending an email).

import { Effect } from "effect";
import { NotifierLive } from "./somewhere";
import { EmailClientLive } from "./somewhere"; // The REAL email client

// ❌ WRONG: This test will try to send a real email.
it("sends a real email", () =>
  Effect.gen(function* () {
    const notifier = yield* Notifier;
    yield* notifier.notifyUser(123, "This is a test email!");
  }).pipe(
    Effect.provide(NotifierLive),
    Effect.provide(EmailClientLive), // Using the live layer makes this an integration test
    Effect.runPromise
  ));