Skip to main content
skill CX Codex

service-virtualization — Claude Code skill: what it does

Guides test isolation strategy for external dependencies: from MSW and WireMock stubs to Testcontainers and Toxiproxy fault injection in CI.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add petrkindlmann/qa-skills --skill service-virtualization -a claude-code
Open on GitHub

What service-virtualization does

This skill provides a decision framework for isolating third-party services in automated tests. It routes each dependency to the appropriate layer so tests do not break on SDK updates. Frontend and Node.js network calls get routed to MSW, while Playwright tests use page.route. Databases or message queues go to Testcontainers.

The assistant inspects .agents/qa-project-context.md before asking questions. It checks whether Docker runs in CI. It also prioritizes official provider sandboxes over custom stubs. Every stub must include realistic errors like status 429 or 500. Without Docker support in CI, Testcontainers and Toxiproxy will not run, which forces a fallback to in-process MSW.

Contract testing with Pact falls outside this skill's scope. Orchestrating full Docker Compose stacks belongs to test-environments instead.

What it helps with

  • choose an isolation tool for third-party HTTP APIs, databases, or queues
  • prevent unhandled network calls from escaping to real services in CI
  • simulate network latency and connection resets with Toxiproxy
  • intercept browser network traffic in Playwright using page.route

Example prompts for the agent

  • Help me set up HTTP stubs for our external payment API in Node.js tests
  • How do I isolate PostgreSQL and Redis in integration tests using Testcontainers?
  • Simulate 500 errors and network timeouts for an external service with Toxiproxy

How to install service-virtualization

1Skills CLI (Vercel)

npx skills add petrkindlmann/qa-skills --skill service-virtualization -a claude-code

2Manually: put the files in place

~/.claude/skills/service-virtualization/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/service-virtualization/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about service-virtualization

How do I install service-virtualization in Claude Code?

Run npx skills add petrkindlmann/qa-skills --skill service-virtualization -a claude-code. The skill file will be placed in ~/.claude/skills/service-virtualization/SKILL.md.

What does service-virtualization do?

It provides a decision tree to choose the right isolation strategy for each dependency, recommending in-process mocks, HTTP stubs like MSW or WireMock, ephemeral Testcontainers, or fault injection via Toxiproxy.

When should I use contract-testing instead of service-virtualization?

Use contract-testing for consumer-driven contract verification with tools like Pact. This skill focuses on choosing stubs, ephemeral containers, and preventing leaked network calls in CI.

Source text

SKILL.md · MIT
---
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
---

<objective>
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.
</objective>

## Quick Route

| Situation | Go to |
|-----------|-------|
| Node/browser test hitting an external HTTP API | MSW → `references/msw.md` |
| Browser page driven by Playwright, calling its own backend under `/api/*` | `page.route` in the test runner (reverse registration order, `route.fallback()` to chain, an unhandled-request guard that aborts and logs); MSW browser mode needs the application to serve `mockServiceWorker.js`, which a frozen build does not. Cassettes: `references/record-replay.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 and is filled in, use it (an unfilled template answers nothing), 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 → Same process as the test? In-process mock (vi.mock / jest.mock / monkeypatch)
│        A browser page calling its own HTTP backend → stub at the HTTP layer (page.route)
└─ 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 intercepts HTTP in the browser or Node. The reference targets the pinned **2.15.0** API;
check the installed major before copying it, since MSW 3 changes imports and options.
Stub at the HTTP layer (`POST /v1/payment_intents`), never at the SDK method — that decouples
the test from the SDK version.

For MSW 2, use `setupServer(...).listen({ onUnhandledRequest: "error" })` to reject intercepted
requests without a matching handler. Prove both an expected mock response and a deliberate
unhandled rejection. Explicit passthrough, uncaptured transports, or application code that
swallows the rejection need separate checks; a green suite or empty log search is not an
egress guarantee.

Put opt-in error handlers before a matching success handler. Rebuild stateful factories
between tests: `resetHandlers()` alone does not clear their captured maps. See
`references/msw.md` for payment handlers, explicit local fallback, state reset, an executable
sample client with timeout/retry cases, and a 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) when one is
installed. When none is and a new dependency is not allowed, or when the steps include responses
the UI never triggers on its own (a 429 after repeated failures, a 409 on a changed replay, a 404),
a short recorder that writes the cassette format in `references/record-replay.md` through the
test runner's request context is the right tool; Playwright's `routeFromHAR` captures only what
the page requests.

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. Age is a proxy; a fidelity check that replays every recorded
request against the live provider and compares the response shape is the evidence. Where that
check runs in the same suite, schedule the recorder from it so the stamp never reaches the limit,
rather than letting a green suite turn red on a date.

See `references/record-replay.md` for the cassette format, the `assertFresh()` 30-day expiry check,
the fidelity check against the live provider, and a replay harness driving a multi-step flow
(create order → add items → apply coupon → checkout).

---

## CI Integration

MSW needs no separate stub server. Set `onUnhandledRequest: "error"` in CI, assert the expected
mock response was observed, and exercise a deliberately unhandled request to prove the guard.
Record unexpected bypasses; use CI network controls when zero egress is required. 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`).

### 8. A success handler shadows the error scenario
MSW stops at the first matching handler that returns a response. A rate-limit handler placed
after the create-payment handler never gets to return 429. Put opt-in errors first, then
verify both the error and ordinary success request against the same endpoint.

### 9. Resetting handlers while retaining fixture state
`server.resetHandlers()` removes runtime overrides, but existing closures still hold payment
or session maps. Recreate each stateful handler factory between tests and verify a previously
existing record is absent afterward. Use IDs unique within the fixture instead of timestamps
that collide under a frozen clock.

---

## Verification

Verify the actual code and observed requests, smallest check first:

```bash
# In this skill repository: execute the actual MSW reference code fences.
node --test --test-isolation=none tests/examples/msw.test.mjs
```

The regression must observe a healthy create/retrieve/confirm flow, the opt-in 429, a client
timeout after its handler was reached, bounded 503 retries, fresh state after reset, and a
deliberate unhandled request rejected before fallback. Moving the rate-limit handler behind
success or retaining the old state must make the corresponding case fail.

For a consumer project, run its actual integration command with CI settings and verify the
same signals. An absent provider hostname in logs proves only that the search found no line;
mocked requests themselves can log provider URLs. Inspect matched/unhandled/bypassed request
evidence and network enforcement separately. For container fixtures, verify healthy startup
and guaranteed teardown. State which transports and scenarios were exercised; this recipe
test does not validate a live provider contract.

---

## 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 with the installed framework's unhandled-request guard, an expected
  mocked request was observed, and an intentionally unhandled request proves the guard rejects
  fallback. Unexpected bypasses are accounted for; zero-egress claims have network evidence.
- Stateful fixtures and runtime overrides reset between tests; a record created before reset
  is absent afterward, and a prior error override no longer affects the next test.
- 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** — ordered payment/error handlers, Vitest setup with an explicit local fallback
  switch and state reset, executable timeout/retry examples, and create/verify/refresh/revoke auth.
- **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.

© kindlmann · MIT · petrkindlmann/qa-skills

Similar skills

All skills for Testing and QA →

From the same set49

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor