Mocking Strategy skill for Claude Code

Mock the edges, run everything you own. Claude classifies every dependency before reaching for a mock: owned code runs for real, owned databases run in containers, third-party APIs sit behind an adapter and are stubbed at the HTTP layer. In-memory fakes share a contract suite with the real adapter, so a double that drifts from reality fails loudly instead of hiding a production bug.

  • namemocking-strategy
  • size161 lines · ≈1,700 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Decides what to replace with test doubles and how, distinguishing stubs, mocks, spies, and fakes, mocking only at out-of-process and contract boundaries, and stubbing HTTP at the network layer with MSW, respx, or responses. Covers wrapping third-party SDKs in adapters, in-memory fakes shared with contract tests, and Testcontainers for owned databases. Use when tests are full of mocks, when deciding whether to mock the database or an API, when mocks drift from reality, or when setting up HTTP mocking for a test suite.

Try asking

  • Our service tests have 12 mocks each. Help me cut them down
  • Should I mock Postgres or run a real one for these repository tests?
  • Set up MSW so no test can accidentally call the real GitHub API

Use it when

  • Tests are dominated by mock setup and call assertions
  • Deciding whether to mock a database, API, or SDK
  • A production bug slipped past tests because a mock lied
  • Introducing HTTP-level mocking to a suite

Skip it when

How to check the result

Running the suite with unhandled network requests set to error passes, and changing the real adapter's behavior makes its shared contract test fail for both fake and real.

The common mistake it prevents

Mocking a third-party SDK's methods directly in dozens of tests, so every SDK upgrade breaks the suite while real API changes go unnoticed.

~/.claude/skills/mocking-strategy/SKILL.md

--- name: mocking-strategy description: Decides what to replace with test doubles and how, distinguishing stubs, mocks, spies, and fakes, mocking only at out-of-process and… license: MIT ---

Mock what you do not control and cannot run cheaply; run what you own. A test double is justified when the real thing is slow, nondeterministic, costly, or has side effects outside the process. Every double is a claim about how the real dependency behaves, and that claim needs its own verification.

Vocabulary (use it precisely)

DoubleWhat it doesAssert on
DummyFills a parameter, never usedNothing
StubReturns canned answers to callsThe system’s output, not the stub
SpyRecords calls to a real or stub implementationCalls, when the call is the behavior
MockPre-programmed with expectations; verifies callsThe interaction itself
FakeWorking lightweight implementation (in-memory repo, fake clock)System output; the fake behaves like the real thing

Rule of thumb: stubs for incoming data (queries), mocks/spies only for outgoing side effects that are the point of the behavior (email sent, event published, payment charged).

Decision table

DependencyDefaultWhy
Your own pure classes/functionsUse the real thingMocking them couples tests to structure
Your own database (Postgres, MySQL, Redis)Real instance via Testcontainers or a CI service, per-test transaction or truncationOnly the real engine has real constraints, SQL dialect, and locking
SQLite standing in for PostgresAvoidDifferent types, constraints, and SQL; tests pass, prod fails
Third-party HTTP API (Stripe, GitHub)Stub at HTTP layer (MSW / respx / responses / WireMock) behind your own adapterDeterministic, offline; adapter isolates the SDK
Message broker publishFake or spy on your publisher adapterAssert the outgoing message shape
Clock, randomness, UUIDsInject a fakeDeterminism
FilesystemReal temp directory (tmp_path, fs.mkdtemp)Cheap and real
Environment variablesStub via framework helpers that auto-restoreIsolation
Framework internals (router, ORM session)Do not mock; use the framework’s test utilitiesMocks miss real behavior

Workflow

- [ ] 1. List the dependencies the unit under test touches
- [ ] 2. Classify each: owned in-process / owned out-of-process / third-party / nondeterministic
- [ ] 3. Apply the decision table
- [ ] 4. Put third-party calls behind an adapter you own
- [ ] 5. Stub at the lowest stable layer (HTTP, not SDK methods)
- [ ] 6. Verify each double against reality (contract or adapter integration test)

Adapter, then stub at the boundary

“Don’t mock what you don’t own”: do not mock stripe.paymentIntents.create across 40 tests. Wrap it:

// src/payments/payment-gateway.ts
export interface PaymentGateway {
  charge(input: { amountCents: number; currency: string; customerId: string; idempotencyKey: string }):
    Promise<{ status: 'succeeded' | 'declined'; chargeId?: string }>;
}
  • Business logic tests use an in-memory FakePaymentGateway.
  • The real StripePaymentGateway gets a few integration tests that stub HTTP with MSW (or Stripe’s test mode in a separate, slower job).
  • When the SDK changes, only the adapter and its tests change.

HTTP stubbing with MSW (Node and browser)

// test/msw-server.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('https://api.example.com/users/:id', ({ params }) =>
    HttpResponse.json({ id: params.id, name: 'Ada' })),
];
export const server = setupServer(...handlers);

// test/setup.ts
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// in a test: override for one case
server.use(http.get('https://api.example.com/users/:id', () => new HttpResponse(null, { status: 503 })));

onUnhandledRequest: 'error' turns any unexpected real network call into a test failure.

Python equivalents:

# httpx -> respx
import respx, httpx
@respx.mock
def test_fetch_user():
    respx.get("https://api.example.com/users/1").mock(return_value=httpx.Response(200, json={"id": 1}))
    assert fetch_user(1).id == 1

# requests -> responses
import responses
@responses.activate
def test_fetch_user_requests():
    responses.get("https://api.example.com/users/1", json={"id": 1})
    assert fetch_user(1).id == 1

JVM: WireMock. Go: httptest.NewServer with a handler.

Fakes that stay honest

Write one shared test suite and run it against both the fake and the real adapter:

export function paymentGatewayContract(make: () => Promise<PaymentGateway>) {
  it('declines amounts above the test limit', async () => { /* ... */ });
  it('returns the same charge for a repeated idempotency key', async () => { /* ... */ });
}
// fake-payment-gateway.test.ts   -> paymentGatewayContract(async () => new FakePaymentGateway())
// stripe-payment-gateway.int.ts  -> paymentGatewayContract(async () => new StripePaymentGateway(testKey))

If the real adapter fails a case the fake passes, the fake is lying and every test that uses it is suspect. For service-to-service APIs, use contract-testing.

Smells that mean “too many mocks”

  • A test sets up more than ~3 mocks: the unit has too many collaborators or the test is at the wrong level.
  • Mocks returning mocks (repo.find.mockReturnValue({ save: vi.fn() })).
  • Assertions only on toHaveBeenCalledWith with no assertion on output.
  • Refactoring the call order inside a method breaks tests while behavior is identical.
  • Mocking the module under test’s own helpers.
  • A bug in production that all tests missed because the mock returned a shape the real API never returns.

Fix by moving the decision logic into a pure function, testing that without doubles, and keeping a thin orchestration layer covered by an integration test.

Gotchas

  • Partial mocks (spying on one method of a real object) keep real behavior elsewhere and surprise you when that real behavior does I/O.
  • Mock return shapes drift: generate them from the real schema (OpenAPI types, recorded fixtures refreshed periodically) rather than hand-typing JSON.
  • Recorded HTTP cassettes (VCR, Polly) leak secrets and go stale. Scrub headers and set an expiry for re-recording.
  • Global fetch mocks bypass the request pipeline (headers, retries, auth refresh); stub at the network layer instead.
  • Restore every double after each test (restoreMocks, monkeypatch, resetHandlers).

Verification

  1. With all doubles in place, break the real collaborator’s contract (change a field name in the fake vs. real schema): the contract or adapter test must fail.
  2. Grep the suite for mocks of owned in-process modules and justify or remove each.
  3. Run the suite offline (or with onUnhandledRequest: 'error') to prove no hidden network.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc