| name | service-virtualization |
| description | Decision framework for isolating every external dependency in a test suite: when to use in-process mocks, HTTP stubs (MSW, WireMock), record-replay, fault injection (Toxiproxy), or ephemeral real services (Testcontainers), and how to enforce that no real calls escape in CI. Use when: "mock service," "stub API," "fake service," "WireMock," "MSW," "Toxiproxy," "test isolation," "dependency management," "stub an external API in CI." Not for: consumer-driven contract verification (Pact, broker) — use contract-testing; standing up a full Docker Compose env or seed data — use test-environments; broad resilience/game-day fault campaigns — use chaos-engineering. Related: contract-testing, test-environments, api-testing, test-data-management, chaos-engineering. |
| license | MIT |
| metadata | {"author":"kindlmann","version":"2.0","category":"infrastructure"} |
Mock Stripe at the SDK layer and your tests pass green while production 500s the moment Stripe
changes a response field — because the stub was never tied to a contract. This skill picks the
right isolation strategy per dependency (in-process mock, HTTP stub, record-replay, fault
injection, or ephemeral real service), stubs at the HTTP layer so tests survive SDK upgrades, and
makes the CI run fail when any real call escapes the stubs.
Quick Route
| Situation | Go to |
|---|
| Node/browser test hitting an external HTTP API | MSW → references/msw.md |
| Polyglot CI, complex matching, or you need a standalone stub server | WireMock → references/wiremock.md |
| Dependency is a DB / cache / queue | Testcontainers → references/testcontainers.md |
| Testing timeouts, latency, connection resets | Toxiproxy → references/toxiproxy.md |
| Bootstrapping stubs from a real API, or a multi-step baseline | Record-replay → references/record-replay.md |
| Wiring any of these into GitHub Actions | references/ci.md |
| Not sure which to reach for | Decision Tree (below) |
Discovery Questions
Check .agents/qa-project-context.md first — if it exists, use it, respect existing mocking
conventions, and skip anything answered there. Then:
- How many external dependencies does the system call, and which are painful in tests?
Rate-limited, slow, flaky, or paid dependencies are the highest-priority virtualization
candidates; list payment, email, auth, and third-party data sources.
- What testing levels need isolation? Unit tests want fast in-process mocks; integration tests
want HTTP-level stubs; E2E wants real or containerized services — the level sets the strategy.
- Does the provider offer an official test mode or sandbox? Stripe test mode, Twilio test
creds, etc. beat any home-grown stub for fidelity — prefer them where they exist.
- Do you have contracts with these dependencies? If yes, contract tests keep stubs in sync;
see
contract-testing. If no, mocking what you don't own is a known risk (Core Principle 3).
- Docker available in CI? No Docker steers you to MSW (in-process); Docker unlocks WireMock,
Testcontainers, and Toxiproxy.
Core Principles
1. Match the isolation level to the confidence you need. Unit tests can mock aggressively —
they test internal logic. Integration tests should use realistic stubs or real services because
they test boundaries. E2E should run the closest thing to production that is still reliable.
2. Real services beat fakes when they're fast, free, and reliable. A local PostgreSQL
container gives more confidence than a fake and costs little — prefer it. Reserve fakes for
dependencies that are slow, unreliable, or paid.
3. Never mock what you don't own without a contract — and prefer the provider's test mode. If
you stub Stripe and Stripe changes its response shape, your tests stay green while production
breaks. In order of preference: use the provider's official test mode/sandbox (Stripe test mode,
Twilio test creds) → an HTTP stub validated by a contract test for drift → a bare stub only when
nothing better exists. See contract-testing.
4. Stubs must fail realistically. A stub that always returns 200 never exercises error
handling. Every stub needs error variants: 429 rate limits, 500s, timeouts, malformed bodies.
5. One abstraction layer between test and tool. Wrap MSW handlers, WireMock stubs, and
Testcontainers setup behind a consistent interface so switching tools doesn't mean rewriting tests.
Decision Framework
When to use each isolation strategy
| Strategy | Speed | Fidelity | Complexity | Best for |
|---|
| In-process mock | Fastest | Lowest | Trivial | Unit tests, isolating internal modules |
| HTTP stub (MSW) | Fast | Medium | Low | Frontend/Node tests hitting external APIs |
| HTTP stub (WireMock) | Fast | Medium-High | Medium | Language-agnostic, complex matching rules |
| Record-replay | Fast after first run | High initially, decays | Medium | Bootstrapping stubs from real APIs quickly |
| Service fake | Medium | High | High | Stateful dependencies (in-memory DB, fake auth) |
| Ephemeral real (Testcontainers) | Slower | Highest | Medium | Databases, message queues, caches |
| Shared real service | Slow | Production-level | Low (to set up) | Staging validation, final pre-deploy check |
Decision tree
Is the dependency internal to your codebase?
├─ Yes → In-process mock (vi.mock / jest.mock / monkeypatch)
└─ No → Is it a database, cache, or message queue?
├─ Yes → Testcontainers (ephemeral real instance)
└─ No → Is it a third-party HTTP API?
├─ Yes → Does the provider offer a test/sandbox mode?
│ ├─ Yes → Use sandbox in staging, MSW/WireMock in CI
│ └─ No → MSW or WireMock + contract test for drift detection
└─ No → Is it an internal microservice?
├─ Yes → Contract test (Pact) + stub for consumer tests
└─ No → Evaluate case by case
Tools
Pick MSW as the default for in-process Node/browser tests; WireMock for cross-language CI
or standalone stub servers; Prism when the OpenAPI spec is the contract; Mockoon for
dev-time exploratory mocking. Heavy code for each lives in references/ — pointers below.
MSW (Mock Service Worker)
MSW 2.x intercepts HTTP at the network layer (Service Worker in the browser, request interception
in Node). The default for JS/TS projects. Stub at the HTTP layer (POST /v1/payment_intents),
never at the SDK method — that decouples the test from the SDK version.
The strongest enforcement seam this skill offers: setupServer(...).listen({ onUnhandledRequest }).
Set it to "error" in CI so any escaped real call fails the run; "warn" is fine locally while
iterating.
See references/msw.md for centralized stateful handlers (payments), the Vitest setup with the
CI/local onUnhandledRequest switch, per-test timeout/retry overrides, and a full stateful auth
flow (create / verify / refresh / revoke via http.delete) keyed on a Bearer token.
WireMock
Language-agnostic HTTP stub server (current stable 3.13.2 — 4.0 is beta-only as of mid-2026,
stay on 3.x for CI). Runs standalone or as a Docker container. Best for polyglot environments or
complex matching rules.
Use priority-based mappings for error scenarios: a priority: 1 mapping that matches an
opt-in header (X-Test-Scenario: rate-limit) and returns 429 shadows the default happy-path
mapping only when the test asks for it. WireMock also exposes an admin API for programmatic stub
creation (POST /__admin/mappings), verification (POST /__admin/requests/count), and reset
(POST /__admin/mappings/reset).
See references/wiremock.md for the Docker setup, a response-templated paginated mapping, the
priority error mapping JSON, and the drift-detection seam to contract-testing (replay a Pact or
validate the stub against the OpenAPI spec via Prism).
Other HTTP mock servers
| Tool | Strengths | When to use |
|---|
| Mockoon | Desktop UI + CLI; OpenAPI import; rule-based responses; lightweight | Dev-time mocking and quick CLI mocks in CI |
| Hoverfly | Capture-replay (record real traffic, replay deterministically); capture/simulate/modify/synthesize modes | Migrating from a real dependency to a mock — record once, replay forever |
| Prism | OpenAPI-driven mock server (Stoplight); validates requests + generates responses from spec | OpenAPI-first projects with a published spec |
| MockServer | Java-based; rich expectation matching; multi-protocol | JVM teams already on MockServer |
Testcontainers
Spin up real services in Docker for integration tests — containers start before the suite and
are destroyed after. Use @testcontainers/postgresql 11.x and friends. Ports are random;
always read them via getMappedPort() / getConnectionUri(), never hardcode 5432/6379.
See references/testcontainers.md for parallel PostgreSQL + Redis + Elasticsearch startup with
wait strategies, the Vitest globalSetup wiring (process.env.DATABASE_URL, testTimeout: 30_000),
and the note on refreshing aging image tags (elasticsearch:8.12.0 is behind Elastic 9.x).
Toxiproxy (fault injection)
ghcr.io/shopify/toxiproxy:2.12.0 sits between your app and a dependency as a TCP proxy and
injects latency, bandwidth limits, and connection resets. Point your app at the proxy port,
not the real service. Always remove toxics in afterEach — they leak across tests otherwise.
See references/toxiproxy.md for the compose port mapping, the createProxy(name, listen, upstream) wiring (including how the compose 15432/16379 proxy ports map to the real upstream),
the helpers with response.ok checks on every call, and a latency + connection-reset usage example.
For broad resilience/game-day work, go to chaos-engineering instead.
Record-Replay
Record-replay captures real API responses once and replays them deterministically — good for
bootstrapping stubs and for a regression baseline of a multi-step interaction. Implement it with a
record-replay library (Hoverfly, Polly.JS, or VCR-style cassettes), not a hand-rolled
recorder.
It breaks on dynamic data (timestamps, UUIDs), stateful sequences, and age — recordings go stale
within weeks. Always stamp a recordedAt and fail the test when a recording is older than 30
days, forcing a re-record.
See references/record-replay.md for the cassette format, the assertFresh() 30-day expiry check,
and a replay harness driving a multi-step flow (create order → add items → apply coupon → checkout).
CI Integration
MSW needs zero infrastructure — it intercepts in-process, so CI runs exactly like local; the only
rule is to set onUnhandledRequest: error in CI (quoted "error" in JS) so an escaped real call
hard-fails the run. WireMock and Testcontainers need Docker.
Two incompatible port models coexist and must not be mixed in one suite: the docker-compose
model publishes fixed ports (a hardcoded DATABASE_URL=...localhost:5432... works), while the
Testcontainers model uses random ports read via getMappedPort() and injected into
process.env. Pick one per suite.
See references/ci.md for the MSW step, the docker-compose GitHub Actions job (up -d --wait --wait-timeout 120, if: always() teardown), and the tool-by-constraint table.
Anti-Patterns
1. Mocking everything
If every dependency is mocked, your tests verify that your mocks work, not that your system works.
Use real services for databases and caches (via Testcontainers); only stub external HTTP APIs.
2. Inconsistent mock behavior across tests
One test stubs Stripe as { id: "pi_123" }, another as { paymentIntentId: "pi_123" } — now you
have two conflicting versions of reality. Centralize handlers and reuse one response shape across
the whole suite (see the shared shape in references/msw.md).
3. Not updating stubs when the API changes
Your mapping says Stripe returns { amount: 1000 } but the real API now returns
{ amount: 1000, currency: "usd" }. Tests pass, production fails. Use contract tests to detect
drift — see contract-testing and the drift seam in references/wiremock.md.
4. Stubbing the wrong layer
Mocking stripe.paymentIntents.create (the SDK method) couples the test to the SDK version. Stub
at the HTTP layer (POST /v1/payment_intents) so the test works regardless of HTTP client or SDK
version.
5. No error-scenario coverage
Stubs that always return 200 never exercise retry logic, timeout handling, rate-limit backoff, or
error parsing. Every stub needs a corresponding error variant.
6. Shared, long-lived mock servers
A shared WireMock instance that multiple CI jobs hit introduces coupling and state leakage. Each
test run starts its own isolated stub server.
7. Record-replay without expiration
Recordings from six months ago reflect an API that no longer exists. Stamp recordedAt and fail
the test when recordings exceed 30 days, forcing a re-record (see references/record-replay.md).
Verification
Prove no real call escaped, smallest check first:
CI=1 npm run test:integration
docker compose -f docker-compose.test.yml up -d --wait --wait-timeout 120 && echo OK
docker compose -f docker-compose.test.yml down -v
grep -iE "api\.stripe\.com|api\.twilio\.com" ci-run.log && echo "LEAK" || echo "clean"
A green run under CI=1 with onUnhandledRequest:"error" plus an empty grep for the real host is
the proof that the suite ran fully virtualized.
Done When
- A dependency isolation strategy is decided and documented for each external dependency (which get
MSW/WireMock stubs, which use Testcontainers, which use the provider's sandbox mode).
- Stubs cover all critical external dependencies with at least one error path each (4xx/5xx,
timeout, or rate limit).
- Stubs and mapping files are versioned alongside test code in the same repository.
- The suite runs green in CI with
onUnhandledRequest: "error" (or the WireMock equivalent), and a
grep of CI logs for the real provider host returns nothing.
- Any record-replay baseline carries a
recordedAt stamp and a 30-day expiry check that fails the
test when stale.
Reference Files (in references/)
- msw.md — centralized stateful handlers, Vitest setup with the CI/local
onUnhandledRequest
switch, per-test timeout/retry overrides, and the full create/verify/refresh/revoke auth flow.
- wiremock.md — Docker setup, response-templated and paginated mappings, the priority-based
error mapping, the admin API, and the contract-drift seam (Pact/Prism).
- testcontainers.md — parallel PostgreSQL/Redis/Elasticsearch startup,
globalSetup wiring,
and the image-tag refresh note.
- toxiproxy.md — compose ports,
createProxy upstream wiring, helpers with response.ok
checks, and a latency + reset usage example.
- record-replay.md — tooling choices, the cassette format, the 30-day
assertFresh check, and
a multi-step replay harness.
- ci.md — MSW (zero-infra), the docker-compose GitHub Actions job, the two port models, and the
tool-by-constraint table.
Related Skills
- contract-testing — Consumer-driven contract verification with Pact/broker; go there to prove
a stub matches a real provider, not just to detect drift against a shared schema.
- test-environments — Full Docker Compose env strategy, preview environments, and seed data; go
there for standing up the environment, not for isolating a single dependency.
- chaos-engineering — Broad fault-injection campaigns, game days, and blast-radius limits; go
there when resilience itself is the goal rather than making one dependency misbehave in a test.
- api-testing — REST/GraphQL testing patterns, schema validation, and auth flow testing.
- test-data-management — Factory patterns and data seeding for stub state setup.