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.
- name
mocking-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
- You need to verify an API agreement between two services: use contract-testing
- You need framework-specific mock syntax for Vitest: use vitest-testing
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.
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)
| Double | What it does | Assert on |
|---|---|---|
| Dummy | Fills a parameter, never used | Nothing |
| Stub | Returns canned answers to calls | The system’s output, not the stub |
| Spy | Records calls to a real or stub implementation | Calls, when the call is the behavior |
| Mock | Pre-programmed with expectations; verifies calls | The interaction itself |
| Fake | Working 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
| Dependency | Default | Why |
|---|---|---|
| Your own pure classes/functions | Use the real thing | Mocking them couples tests to structure |
| Your own database (Postgres, MySQL, Redis) | Real instance via Testcontainers or a CI service, per-test transaction or truncation | Only the real engine has real constraints, SQL dialect, and locking |
| SQLite standing in for Postgres | Avoid | Different 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 adapter | Deterministic, offline; adapter isolates the SDK |
| Message broker publish | Fake or spy on your publisher adapter | Assert the outgoing message shape |
| Clock, randomness, UUIDs | Inject a fake | Determinism |
| Filesystem | Real temp directory (tmp_path, fs.mkdtemp) | Cheap and real |
| Environment variables | Stub via framework helpers that auto-restore | Isolation |
| Framework internals (router, ORM session) | Do not mock; use the framework’s test utilities | Mocks 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
StripePaymentGatewaygets 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
toHaveBeenCalledWithwith 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
fetchmocks bypass the request pipeline (headers, retries, auth refresh); stub at the network layer instead. - Restore every double after each test (
restoreMocks,monkeypatch,resetHandlers).
Verification
- 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.
- Grep the suite for mocks of owned in-process modules and justify or remove each.
- 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.