Resource library

QA How-To

Mocking third party APIs in tests (2026)

Master mocking third party APIs in tests with strategy choices, contract drift controls, failure scenarios, webhooks, and runnable Node and Playwright examples.

24 min read | 3,105 words

TL;DR

Mocking third party APIs in tests is most effective when each test double has a defined purpose. Use injected fakes for fast decision logic, network-level stubs for HTTP behavior, contract checks to control drift, and a small sandbox suite for provider reality. Model failures, state, calls, webhooks, and sensitive data, not just a successful response body.

Key Takeaways

  • Choose the mock boundary based on the risk, from injected client fakes for logic to network stubs for serialization and end-to-end wiring.
  • Model status, headers, body, latency, connection behavior, pagination, state, and call history rather than returning one happy JSON object.
  • Keep mock fixtures traceable to provider documentation, captured examples, schemas, or consumer contracts so they do not drift into fiction.
  • Test timeouts, throttling, malformed responses, partial data, duplicate callbacks, and recovery paths deterministically.
  • Treat webhook delivery and polling as stateful protocols with signatures, retries, ordering, and idempotency.
  • Run a small provider sandbox or staging suite to validate assumptions that mocks cannot prove.
  • Never copy live secrets or sensitive customer payloads into fixtures, recordings, logs, or reports.

Mocking third party APIs in tests lets a team verify its own behavior without depending on a provider's availability, rate limits, data, latency, or billing. The mock should create control, not false confidence. A test double that always returns one perfect 200 response makes a suite fast while hiding authentication, schema, pagination, throttling, timeout, webhook, and recovery defects.

A strong strategy uses several boundaries. Fast unit tests replace the client dependency. Component tests serve real HTTP responses from a local stub. Contract checks keep fixtures aligned with provider expectations. A small sandbox or staging suite verifies facts that no mock can prove. This guide shows how to choose among those layers and build deterministic, secure scenarios with runnable Node and Playwright examples.

TL;DR

Approach Best at Does not prove Typical speed
Injected function or client fake Application branching, retries, mapping HTTP serialization or network configuration Fastest
In-process HTTP server Method, path, headers, body, timeout Real provider behavior Fast
Standalone stub server Shared scenarios, non-Node clients, proxy tests Provider deployment and undocumented behavior Fast to medium
Record and replay Realistic examples and discovery Future compatibility, secrets safety by default Medium
Provider sandbox Credentials, real protocol, provider validation Production parity or deterministic failure injection Medium to slow
Production synthetic Critical live path Destructive cases or exhaustive coverage Slow and tightly controlled

Use the narrowest double that can expose the risk, then keep one higher-level check for the wiring you intentionally skipped.

1. Define Mocking Third Party APIs in Tests by Boundary

A mock is often used as a general word for several test doubles. A stub returns predefined results. A fake provides a working but simplified implementation, possibly with in-memory state. A spy records calls. A mock commonly includes expectations about interactions. A simulator aims to reproduce a larger protocol. The label matters less than documenting the behavior and limitation.

Start at the seam your application owns. Wrap the external SDK or HTTP calls in a small gateway such as ShippingRates, FraudDecisionClient, or EmailProvider. Application services depend on that interface, not on global network state. Unit tests can inject a tiny fake and focus on domain decisions. Component tests exercise the real gateway against a network stub and verify URL construction, request encoding, authentication headers, response parsing, timeouts, and errors.

Avoid mocking deep library internals. Replacing a private method inside an HTTP package couples tests to an implementation detail and may bypass the exact serialization code that breaks in production. Mock either your own abstraction or the network boundary. Likewise, intercepting browser calls does not test server-to-server calls that occur in a backend. Map the actual request path before choosing a tool.

Define ownership. Your team owns request construction, status classification, retries, fallback, persistence, and user messaging. The provider owns its real implementation. Your tests should assert your observable contract with that provider, not reproduce every internal provider rule. A smaller explicit model is easier to review and less likely to become an unmaintainable shadow service.

2. Choose a Test Double Strategy by Risk

Select a boundary based on what could fail. If the risk is choosing express shipping when a rate is below a threshold, inject a fake result. If the risk is sending amount as dollars instead of minor units, exercise real JSON serialization through an HTTP stub. If the risk is OAuth configuration or certificate trust, use the provider sandbox. If the risk is the provider changing a response field, run schema or consumer contract checks against current evidence.

Network stubs can run in process, as a sidecar container, or as a shared service. In-process servers give each test isolated state and simple lifecycle. Sidecars work across languages and can model proxies, TLS, and connection behavior. Shared mock environments are easy to start but become contested mutable infrastructure unless scenarios are namespaced and reset safely.

Browser interception is ideal when a frontend calls an external or backend endpoint directly and the test needs stable UI states. Playwright routing can fulfill, abort, or modify requests. It does not automatically cover service calls made outside that browser context. Playwright route fulfill examples demonstrates that boundary in detail.

Use record and replay carefully. A recording captures realism quickly, but it can contain tokens, personal data, timestamps, signed URLs, and unstable headers. Sanitize before storage, review every fixture, and convert important recordings into readable named scenarios. Do not let a cassette silently accept any request and replay an old response, because then request regressions pass unnoticed.

3. Model the HTTP Contract, Not Only JSON

A third-party contract includes method, scheme, host, path, path encoding, query serialization, selected headers, authentication, content type, body, status, response headers, response body, redirects, compression, and timing. Your stub should validate fields the application must send and allow provider-controlled values that legitimately vary.

For a create request, assert method and exact route, required idempotency key, supported media type, and stable body fields. Avoid asserting header order or a generated user-agent string unless it is required. For a response, model content type and headers such as request ID, pagination link, location, rate-limit guidance, cache policy, or retry delay when the client consumes them. A JSON body with status 200 cannot expose code that incorrectly ignores 202, 204, or 429.

Build named scenario fixtures rather than anonymous blobs: approved_standard, declined_insufficient_data, rate_limited_then_success, invalid_json_502, and accepted_webhook_later. Give every scenario an expected request count and meaningful state. When a test fails, the report should show the selected scenario, unmatched request, closest expectation, and sanitized actual body.

Be deliberate about strictness. An overly permissive stub allows missing authentication and malformed requests. An exact full-body snapshot fails whenever a harmless optional field or timestamp changes. Validate the fields and constraints your consumer contract depends on. Schema validation, focused matchers, and explicit ignored fields create a useful middle ground. For reusable HTTP stub patterns, see WireMock stubbing.

4. Keep Mocks from Drifting Away from the Provider

Mock drift occurs when tests describe a provider that no longer exists. The suite stays green because both application assumptions and fixtures are wrong in the same direction. Prevent drift with provenance. Each fixture should identify the provider operation, documentation version or retrieval date, sanitized source example, schema, and owning test. Review fixtures when provider release notes or API specifications change.

If the provider publishes OpenAPI, JSON Schema, protobuf, or an SDK model, validate selected stub responses and client requests against it. Do not assume a published specification covers undocumented edge behavior, and do not regenerate all fixtures blindly. Generated examples often satisfy shape without representing meaningful combinations. Keep hand-designed business scenarios and use the specification as one consistency check.

Consumer-driven contract testing is useful when you control both sides or the provider participates. It records exactly what the consumer needs and verifies the provider against that expectation. For an external provider that will not run your contracts, you can still validate your consumer examples against a sandbox on a schedule. API contract testing with Pact explains the cooperative provider workflow.

Create a thin live validation suite. It should use supported sandbox accounts, minimal calls, unique test references, and safe cleanup. Verify authentication, one or two critical request shapes, response parsing, and a webhook if available. Do not replicate every mocked scenario in the sandbox. Its job is to detect assumption drift and deployment configuration problems, while mocks retain deterministic breadth. Alert on drift separately from product regressions so provider outages do not make every commit unreliable.

5. Simulate Failures, Latency, and Recovery Deterministically

External dependencies fail in more ways than returning 500. Test connection refusal, DNS or TLS failure where your layer can control it, connect timeout, read timeout, slow streaming body, truncated body, invalid JSON, wrong content type, empty response, redirect, 408, 429, 500, 502, 503, 504, and a success response missing a required field. Select cases based on the provider contract and business risk.

A deterministic scenario defines a sequence. For example, the first request returns 503 with Retry-After, the second succeeds. Another accepts a command with 202, returns pending twice to polling, then completes. A payment simulation can commit an effect and drop the response so the client must retry safely. Record request timestamps and count to assert backoff and termination. Inject clocks and sleepers rather than waiting in real time.

Separate transport failure from provider rejection. A card decline or invalid address is a normal business outcome and should not trigger generic retries. A timeout may have an unknown outcome. A 429 asks the client to slow down. A malformed successful response is a provider contract defect and should enter a safe error path, not become default-approved data.

Test fallback honestly. Cached rates may be acceptable for a shipping estimate and dangerous for an account balance. If the UI displays stale data, require a visible freshness indicator. If the service queues work for later, verify durable state and deduplication. If it fails closed, verify no partial write. The mock makes these hard paths repeatable, but the expected behavior must come from a product and architecture decision.

6. Run a Self-Contained Node Test Double Example

This example uses Node's built-in test runner, strict assertions, Response, and dependency injection. It requires a modern Node runtime with the standard Fetch API and no test library. Save the first block as shipping-client.mjs, the second as shipping-client.test.mjs, then run node --test shipping-client.test.mjs.

// shipping-client.mjs
export class ShippingProviderError extends Error {
  constructor(message, { retryable = false } = {}) {
    super(message);
    this.name = "ShippingProviderError";
    this.retryable = retryable;
  }
}

export function createShippingClient({
  baseUrl,
  apiKey,
  fetchImpl = fetch,
  sleeper = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
}) {
  return {
    async quote(postcode, { maxAttempts = 2 } = {}) {
      for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
        const response = await fetchImpl(`${baseUrl}/v1/quotes`, {
          method: "POST",
          headers: {
            authorization: `Bearer ${apiKey}`,
            "content-type": "application/json",
          },
          body: JSON.stringify({ destination: { postcode } }),
        });

        if (response.status === 429 || response.status === 503) {
          if (attempt === maxAttempts) {
            throw new ShippingProviderError(`provider returned ${response.status}`, { retryable: true });
          }
          const delayMs = Number(response.headers.get("retry-after") ?? "0") * 1000;
          await sleeper(Math.min(delayMs, 1000));
          continue;
        }

        if (!response.ok) {
          throw new ShippingProviderError(`provider returned ${response.status}`);
        }

        const data = await response.json();
        if (!Array.isArray(data.rates) || data.rates.length === 0) {
          throw new ShippingProviderError("provider response has no rates");
        }
        return data.rates;
      }
      throw new Error("unreachable");
    },
  };
}
// shipping-client.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
import { createShippingClient, ShippingProviderError } from "./shipping-client.mjs";

function sequenceFetch(steps, calls) {
  return async (url, init) => {
    calls.push({ url, init });
    const step = steps.shift();
    assert.ok(step, "unexpected provider call");
    return new Response(JSON.stringify(step.body), {
      status: step.status,
      headers: { "content-type": "application/json", ...step.headers },
    });
  };
}

test("retries a throttled quote and validates the request", async () => {
  const calls = [];
  const delays = [];
  const fetchImpl = sequenceFetch(
    [
      { status: 429, headers: { "retry-after": "1" }, body: { code: "RATE_LIMITED" } },
      { status: 200, body: { rates: [{ service: "ground", amount: 1299, currency: "USD" }] } },
    ],
    calls,
  );
  const client = createShippingClient({
    baseUrl: "https://shipping.test",
    apiKey: "test-key",
    fetchImpl,
    sleeper: async (ms) => delays.push(ms),
  });

  const rates = await client.quote("94107");

  assert.equal(rates[0].amount, 1299);
  assert.equal(calls.length, 2);
  assert.equal(calls[0].url, "https://shipping.test/v1/quotes");
  assert.equal(calls[0].init.method, "POST");
  assert.equal(calls[0].init.headers.authorization, "Bearer test-key");
  assert.deepEqual(JSON.parse(calls[0].init.body), { destination: { postcode: "94107" } });
  assert.deepEqual(delays, [1000]);
});

test("rejects a successful response with missing rates", async () => {
  const fetchImpl = sequenceFetch([{ status: 200, body: { rates: [] } }], []);
  const client = createShippingClient({
    baseUrl: "https://shipping.test",
    apiKey: "test-key",
    fetchImpl,
  });

  await assert.rejects(
    () => client.quote("94107"),
    (error) => error instanceof ShippingProviderError && error.message.includes("no rates"),
  );
});

The fake records real fetch arguments and returns standard response objects, so parsing code remains active. It does not prove DNS, TLS, socket timeouts, or a provider sandbox. Those risks belong in network and integration layers.

7. Mock Browser Requests with Playwright Routing

When the browser itself requests recommendations, feature flags, maps, or a backend-for-frontend route, Playwright can intercept that request inside a browser context. Register routes before navigation so an early request cannot escape. Match the narrowest stable URL pattern, validate selected request fields, and fulfill a realistic status, content type, headers, and body.

import { test, expect } from '@playwright/test';

test('shows the fallback when recommendations are unavailable', async ({ page }) => {
  await page.route('**/api/recommendations?*', async (route) => {
    const request = route.request();
    expect(request.method()).toBe('GET');
    await route.fulfill({
      status: 503,
      contentType: 'application/json',
      headers: { 'Retry-After': '1' },
      body: JSON.stringify({ code: 'RECOMMENDATIONS_UNAVAILABLE' }),
    });
  });

  await page.goto('http://127.0.0.1:3000/products/42');
  await expect(page.getByRole('status')).toContainText('Recommendations are temporarily unavailable');
  await expect(page.getByRole('heading', { name: 'Product 42' })).toBeVisible();
});

This uses documented page.route, route.request, and route.fulfill APIs. It asserts the intended graceful degradation: the core page remains available while a secondary provider path fails. In a real project, start the local application before the test and change the URL to its configured base URL.

Route interception can accidentally mock too much. A glob that matches every API call may replace authentication, analytics, and first-party data along with the intended dependency. Use exact host and path patterns when possible, and fail unexpected provider calls. Remove or scope handlers per test so state does not leak. To model connection failure rather than an HTTP response, use the framework's supported abort capability and a documented error code, covered in Playwright route abort examples.

8. Model Stateful Protocols, Pagination, and Webhooks

Many providers are not request-response lookups. A job API accepts a command, exposes status through polling, and later sends a webhook. A payment moves through pending, authorized, captured, refunded, and disputed states. A file API paginates with cursors. A useful fake preserves those transitions and rejects illegal ones.

For pagination, return distinct pages with realistic cursor semantics. Test empty first page, exact final page, repeated cursor, missing cursor, duplicate item across pages, item deletion between pages, rate limit midway, and a cycle. The client should stop safely, avoid duplicates according to requirements, and not assume page size proves completion when the contract provides an explicit next token.

Webhook tests need a two-sided model. First, your application sends or receives the initiating API request. Second, the mock provider signs and delivers callbacks. Verify signature validation on raw request bytes, timestamp tolerance, duplicate delivery, out-of-order events, retry after endpoint failure, unknown event type, old object version, and acknowledgement timing. Store provider event IDs so duplicate callbacks do not duplicate effects.

Do not let one global fake state serve parallel tests. Give each test a scenario or account namespace, expose a reset endpoint only in isolated environments, and clean state in finally hooks. Prefer immutable scenario definitions plus per-test runtime state. A test should be able to query call history and current simulated resource state for assertions without reaching into unrelated cases.

9. Protect Secrets, Data, and Test Reliability

Mocks can create security problems because teams treat fixtures as harmless. Recordings may contain authorization headers, cookies, addresses, emails, account numbers, signed URLs, and provider request IDs. Use synthetic data, allowlist fields before recording, replace secrets with fixed placeholders, and scan committed fixtures. Redact before a body reaches logs or reports, not after publication.

Never point failure-injection tests at a live provider by changing one environment variable casually. Add an allowlist for mock and sandbox hosts, mark destructive operations, and make production endpoints impossible in ordinary test configuration. Sandbox calls may still incur quotas, send messages, or create persistent data. Use dedicated accounts and cleanup supported by the provider.

Reliability requires lifecycle discipline. Start the mock before the system under test, wait for a health signal, reset state, execute the case, collect call history, and stop it even after failure. Allocate ports dynamically for parallel local tests or isolate sidecars by worker. A fixed shared port and shared mutable scenario create flakes that look like application bugs.

Keep diagnostics concise. Report unmatched method and path, sanitized request differences, scenario state, and calls received. Avoid dumping megabyte payloads or secret headers. Monitor mock coverage: unexpected real network calls should fail in hermetic suites, while intentionally allowed first-party routes should be explicit. A pass should tell you the system interacted with the expected double, not merely that the UI eventually displayed something.

10. Use a Mocking Third Party APIs in Tests Checklist

For each provider operation, document the test boundary, source of truth, owned behavior, request expectations, response variants, state, cleanup, and higher-level validation. Cover one valid response, important business rejections, authentication failure, authorization failure if applicable, throttling, transient server failure, timeout, malformed or partial success, and an unexpected status. Add pagination, webhook, asynchronous, or idempotency cases when the protocol uses them.

Review realism. Are money values in the right units? Are timestamps and time zones valid? Are optional fields genuinely optional? Are status and headers paired correctly? Does a 204 have no body? Can order vary? Does the stub reject a missing required header? Are volatile response values matched flexibly? Run contract validation over fixtures and a scheduled sandbox smoke test.

Review isolation and safety. Each test receives independent state. Network access is denied except to allowed hosts. Synthetic credentials cannot work against production. Recordings are sanitized and reviewed. Logs omit secrets. Retries and sleeps use controlled time. Unexpected calls fail with useful evidence.

Finally, review value. Delete duplicated scenarios that exercise the same branch. Keep a layered portfolio rather than one enormous simulator. Unit fakes should answer domain questions, network stubs should answer protocol questions, sandbox checks should answer provider-reality questions, and a minimal end-to-end path should answer deployment-wiring questions. When every layer has a purpose, mocks reduce feedback time without pretending the dependency is under your control.

Interview Questions and Answers

Q: What is the main risk of mocking a third-party API?

The mock can drift from the provider while the application and its tests agree with the same false contract. I control that with traceable fixtures, schema or consumer checks, provider documentation review, and a small scheduled sandbox suite.

Q: Where should you place the mock boundary?

I choose the narrowest boundary that still exercises the risk. Domain branching uses an injected client fake, HTTP serialization uses a network stub, browser behavior uses route interception, and credentials or real provider validation require a sandbox.

Q: What failures should a provider mock simulate?

Beyond business rejections, it should cover throttling, selected 5xx responses, timeouts, connection errors, invalid content, partial data, and recovery sequences. The exact list comes from the contract and business impact.

Q: How do you test that the correct request was sent?

The double records method, URL, selected headers, and parsed body, then asserts stable required fields. I avoid brittle checks on header order or volatile values and never expose secrets in failure output.

Q: Can mocks replace contract and sandbox testing?

No. Mocks provide control and speed but cannot prove the provider accepts credentials, honors undocumented rules, or has not changed. Contract and sandbox checks validate selected assumptions at a higher boundary.

Q: How would you test third-party webhooks?

I model the provider as stateful, sign callbacks over the exact raw bytes, and test duplicate, delayed, out-of-order, invalid-signature, unknown-type, and retry scenarios. I assert idempotent application effects and correct acknowledgements.

Q: How do you keep mock tests parallel-safe?

Each test gets a unique scenario namespace and independent runtime state. I start and reset the double deterministically, allocate resources per worker, and query only that test's call history. Shared global scenario mutation is avoided.

The structured interviewQnA field contains shorter model answers for interview drills.

Common Mistakes

  • Returning the same successful JSON for every request and calling the integration covered.
  • Mocking a private method inside an HTTP library, which bypasses owned serialization logic.
  • Using a browser route to claim coverage of backend server-to-server calls.
  • Matching no request fields, so invalid methods, paths, credentials, and bodies still pass.
  • Matching every byte, including volatile timestamps and harmless header differences.
  • Copying recordings with live secrets or personal data into source control.
  • Ignoring status headers, latency, connection failures, pagination, and asynchronous state.
  • Treating provider business rejection as a transient failure that should be retried.
  • Sharing one mutable mock scenario across parallel workers.
  • Letting unknown network calls escape a supposedly hermetic test suite.
  • Building a full provider clone that costs more to maintain than the consumer behavior it tests.
  • Skipping sandbox checks, which allows fixtures and provider behavior to drift silently.

Conclusion

Mocking third party APIs in tests works when the double is small, purposeful, state-aware, and connected to a source of truth. Use dependency injection for fast logic, network stubs for protocol behavior, browser routing for frontend states, and controlled scenarios for failures and webhooks.

Then protect the strategy from its predictable weaknesses. Validate important examples, run a narrow sandbox suite, isolate state, block unexpected network traffic, and sanitize every fixture. The result is a fast test portfolio that exposes how your application handles provider reality without making daily feedback depend on the provider.

Interview Questions and Answers

How do you choose between a fake client and a mock server?

I start from the risk. A fake client is ideal for application decisions and retry policy, while a mock server exercises real HTTP construction and parsing. I keep a sandbox check for facts owned by the provider.

What is contract drift in API mocking?

Contract drift occurs when the mock no longer represents the provider, so application and tests agree on an outdated assumption. I use traceable fixtures, specifications or consumer contracts, change review, and scheduled provider validation to detect it.

What should a realistic third-party mock model?

It should model the request fields the consumer must send, relevant statuses and headers, response shapes, latency, failure sequences, state, and call history. Pagination, polling, idempotency, or webhooks are included when the protocol needs them.

How do you verify requests without making mocks brittle?

I assert required method, route, authentication presence, content type, and stable semantic body fields. I allow volatile values and irrelevant header ordering. Failure reports show sanitized differences only.

How would you test provider retry behavior?

I create a stateful sequence such as 429 with retry guidance followed by success, inject the sleeper and clock, and record calls. I assert status classification, delay bounds, attempt limit, final result, and duplicate-effect protection.

How do you test a webhook integration with a mock provider?

The fake creates provider state and sends callbacks signed over exact raw bytes. I test invalid signatures, duplicates, out-of-order delivery, retries, old versions, and unknown event types. The main oracle is correct acknowledgement plus one business effect.

What security controls apply to API mocks?

I use synthetic data and inert credentials, block unintended hosts, sanitize recordings before storage, redact logs, isolate test accounts, and make production endpoints unreachable from normal failure tests. Mock infrastructure receives lifecycle and access controls like other test systems.

Frequently Asked Questions

What does mocking third party APIs in tests mean?

It means replacing an external provider at a controlled boundary with a test double that returns and records defined behavior. The purpose is deterministic testing of your application without depending on the real provider for every case.

Should I mock the SDK or the HTTP network?

Mock your own wrapper for domain unit tests and use a network stub when request serialization, headers, status handling, or parsing matters. Avoid mocking private SDK internals because those tests are brittle and may bypass production behavior.

How do I prevent an API mock from drifting?

Trace fixtures to documentation, schemas, sanitized examples, or consumer contracts, and review them when the provider changes. Run a small scheduled sandbox validation suite for assumptions that mocks cannot prove.

Which third-party API failures should be mocked?

Prioritize business rejections, authentication errors, 429, selected 5xx responses, timeouts, malformed or partial bodies, and recovery sequences. Add pagination and webhook failures when the integration uses those protocols.

Can Playwright mock a third-party API?

Yes, when the request is made through the routed browser context. Playwright can fulfill, continue, or abort matched requests. It does not automatically intercept server-to-server traffic from your backend.

Is record and replay safe for API testing?

It can be useful, but recordings must be sanitized, reviewed, and matched against meaningful request fields. Old recordings can hide provider drift and may contain tokens, personal data, or unstable values.

Do mocks remove the need for a provider sandbox?

No. Mocks supply deterministic breadth, while a sandbox validates credentials, provider-side validation, deployment configuration, and current response behavior. Keep the sandbox suite small and safe.

Related Guides