Skip to content

Instantly share code, notes, and snippets.

@Maxiviper117
Created August 28, 2025 14:54
Show Gist options
  • Save Maxiviper117/9dd61c5634ae5e95089337fb445ec31a to your computer and use it in GitHub Desktop.
Save Maxiviper117/9dd61c5634ae5e95089337fb445ec31a to your computer and use it in GitHub Desktop.
Type‑Safe Fail‑Fast Mocks for Effect‑TS Services when using Layer.Mock

🔹 makeThrowingMock — Type‑Safe Fail‑Fast Mocks for Effect‑TS Services

When mocking an Effect.Service or Context.Tag in tests,
you often want full type safety while providing only a subset of method overrides.

makeThrowingMock creates a fail‑fast default implementation where every method throws by default,
and you selectively override only what you need per test case.


📦 Implementation

import { Effect } from "effect";

/**
 * Creates a fail-fast mock object for a service where every method throws
 * an error if not explicitly overridden.
 *
 * @param serviceName - The name of the service (used in error messages).
 * @returns An object with the same shape as the service, where every method
 *          is an Effect that dies with a "not implemented" message.
 */
export function makeThrowingMock<T extends object>(
  serviceName: string
): { [K in keyof T]: T[K] } {
  return new Proxy({} as T, {
    get(_, prop) {
      return () =>
        Effect.dieMessage(
          `${serviceName}.${String(prop)} not implemented in this mock`
        );
    },
  });
}

🛠 Usage Example — Logger Service

Here’s a complete example showing how to define an Effect.Service and create a mock with makeThrowingMock.

import { Layer, Effect } from "effect";

// 1. Define the service
class Logger extends Effect.Service<Logger>()("Logger", {
  accessors: true,
  effect: Effect.succeed({
    info: (msg: string) => Effect.sync(() => console.log(msg)),
    warn: (msg: string) => Effect.sync(() => console.warn(msg)),
    error: (msg: string) => Effect.sync(() => console.error(msg)),
  }),
}) {}

// 2. Create a fail-fast mock and override only what you need
const LoggerMock = Layer.mock(
  Logger,
  {
    ...makeThrowingMock<Logger>("Logger"),
    info: (msg: string) => Effect.sync(() => console.log(`[MOCKED]: ${msg}`)),
  }
);

// 3. Use in tests
const program = Logger.info("hello");
Effect.runSync(program); 
// → [MOCKED]: hello

Effect.runSync(Logger.warn("oops"));
// → 💥 throws: Logger.warn not implemented in this mock

✅ Benefits

  • Type‑Safe — If the service signature changes, TypeScript will tell you.
  • Fail‑Fast — Unimplemented methods throw immediately in tests.
  • Minimal Boilerplate — Override only the methods you care about.

🧠 Pro Tip — With Inline Overrides

For extra convenience, wrap the throwing base with overrides in one call:

export function makeThrowingMockWithOverrides<T extends object>(
  serviceName: string,
  overrides: Partial<T>
): T {
  return { ...makeThrowingMock<T>(serviceName), ...overrides } as T;
}

const LoggerMock2 = Layer.mock(
  Logger,
  makeThrowingMockWithOverrides<Logger>("Logger", {
    info: (msg) => Effect.sync(() => console.log(`[MOCKED]: ${msg}`)),
  })
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment