service-virtualization

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,"

By petrkindlmann · 599 installs

npx skills add petrkindlmann/qa-skills --skill service-virtualization

Source repository · Upstream listing

<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 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 r