Mocking External APIs in Tests Instead of Risking Real Calls
AI generated
PASS
expect()
API Mocking · MSW · WireMock · E2E Testing
Mocking External APIs in Tests Instead of Risking Real Calls
Mock servers, fixtures, and error scenarios for stable E2E suites

Hitting payment and shipping providers live in every test run risks cost, rate limits, and random failures caused by someone else's infrastructure. This article shows how mock servers such as MSW and WireMock, recorded fixtures, and deliberately simulated errors and timeouts keep an E2E suite independent of external uptime while still staying realistic.

13 min read MSW · WireMock · cy.intercept Fixtures · Contract Testing · Magento 2.4.8

1. Why real API calls in tests are risky

A payment provider's sandbox account has rate limits, a shipping provider counts every request against a quota, and an email provider actually sends a message on every test run. Hitting these services directly from an E2E suite ties your own test runtime and reliability to a third party's infrastructure. If a payment sandbox goes down overnight for maintenance, dozens of tests suddenly fail for reasons that have nothing to do with your own code.

On top of that come side effects that are hard to keep clean in a sandbox: test orders that generate real shipping labels, or repeated test payments that make the sandbox data set messier over time. Rate limits produce 429 responses under parallelized CI runs that have nothing to do with an actual bug, yet still turn the suite red. A team that sees enough of these failures starts reflexively ignoring red tests, and that is exactly when the suite loses its purpose.

2. Mock Service Worker (MSW): mocking at the network level

Mock Service Worker intercepts HTTP requests at the network level, via a service worker in the browser or an interception layer in Node.js, and answers them with defined handlers. The key advantage over mocking fetch or axios directly in application code: the app itself notices nothing, it sends a genuine request that simply never leaves the machine. This works transparently regardless of which HTTP library a frontend team uses.

MSW is particularly useful when a payment widget or a shipping cost calculator talks to the external API directly in the browser, for example an embedded payment iframe that validates prices client side. Handlers are defined as a list of request matchers with a matching response function and can be overridden per test case, so a single test can simulate a deviating error case without touching the global handler configuration.


// src/mocks/handlers.js
import { http, HttpResponse, delay } from 'msw';

export const handlers = [
  // Happy path: successful payment authorization
  http.post('https://api.payment-provider.test/v2/authorize', async () => {
    return HttpResponse.json({
      status: 'authorized',
      transactionId: 'txn_8f2c1a',
      amount: 4990,
      currency: 'EUR',
    });
  }),

  // Shipping rate lookup for a Magento checkout step
  http.get('https://api.shipping-provider.test/rates', ({ request }) => {
    const url = new URL(request.url);
    const zip = url.searchParams.get('zip');
    if (!zip) {
      return HttpResponse.json({ error: 'missing_zip' }, { status: 400 });
    }
    return HttpResponse.json({
      rates: [{ carrier: 'DHL', service: 'standard', price: 4.99 }],
    });
  }),
];

3. WireMock-style server mocks for backend tests

Not every external API call happens in the browser. In a Magento store, the PHP backend often calls the payment provider or shipping carrier server side, for instance inside a payment module during the order placement pipeline. MSW does not help here, because the request never touches the browser. This case needs a standalone mock server following the WireMock approach: a separate process or container that accepts HTTP requests and answers them based on configured stub mappings, while the Magento container points to that mock server instead of the real API via a DNS or configuration override.

WireMock mappings are defined as JSON files with a request matcher, request, and a matching response. They can be changed dynamically at runtime through an admin API. That enables stateful scenarios, for example a first request that succeeds and an identical second request that returns a simulated server error, to verify retry logic. For a Magento test setup, such a mock server typically runs as an extra service in the CI pipeline's Docker Compose configuration.


{
  "request": {
    "method": "POST",
    "urlPath": "/v2/authorize",
    "bodyPatterns": [
      { "matchesJsonPath": "$.currency", "equalTo": "EUR" }
    ]
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": {
      "status": "authorized",
      "transactionId": "txn_8f2c1a",
      "amount": 4990,
      "currency": "EUR"
    },
    "fixedDelayMilliseconds": 120
  }
}

4. Recording and replaying real responses as fixtures

Handwritten mock responses almost always contain subtle deviations from reality: a missing field, a different data type, a nesting level nobody thought of. It is more reliable to record real responses once against the provider's sandbox environment and store them as fixtures. WireMock supports a proxy recording mode for this, forwarding requests to the real API, capturing the response, and automatically saving it as a mapping file. Playwright offers a comparable mechanism through HTTP archives with routeFromHAR().

The advantage over handwritten fixtures is shape fidelity: headers, nested objects, and even unexpected extra fields from the real API end up in the fixture automatically. The downside is that a recorded fixture silently goes stale when the API changes, without any test noticing, as long as the mock is only ever tested against itself. Recorded fixtures therefore belong in the repository under version control and should be re-recorded against the sandbox at regular intervals.


{
  "_recordedAt": "2026-06-02T09:14:00Z",
  "_source": "sandbox.shipping-provider.test",
  "request": { "method": "GET", "path": "/rates?zip=10115&country=DE" },
  "response": {
    "status": 200,
    "body": {
      "rates": [
        { "carrier": "DHL", "service": "standard", "price": 4.99, "etaDays": 2 },
        { "carrier": "DHL", "service": "express", "price": 12.5, "etaDays": 1 }
      ],
      "requestId": "req_a91cf0"
    }
  }
}

5. cy.intercept and page.route right inside the test framework

For many E2E tests, a lighter solution than a separate mock server is enough: both Cypress with cy.intercept() and Playwright with page.route() can intercept HTTP requests directly inside the test framework, with no extra process needed. That works whenever the call to be mocked happens in the browser, for instance a client side payment SDK that validates prices before checkout. Both APIs let you assign a specific fixture or callback response per test case, without touching the handler configuration for the whole project.

The important difference from MSW or WireMock: cy.intercept() and page.route() only work within the running test framework and only for requests the browser actually sends. If the backend calls an external API server side, that path stays invisible to the test framework and must be covered separately through a server side mock like WireMock. In practice, many Magento projects combine both approaches: cy.intercept() for client side calls, WireMock for server side payment and shipping integrations.

6. Simulating error and timeout scenarios on purpose

It is nearly impossible to reliably force a real sandbox to answer with 503, abort a request after ten seconds, or suddenly return malformed JSON in the response body. Yet those exact scenarios are what matters most for checking whether an application shows understandable errors, whether retry logic kicks in, and whether timeout budgets are honored. Mocking makes these states deterministic and free of randomness, every test run produces exactly the same failure case.

cy.intercept() supports options such as forceNetworkError for a complete connection failure and delay for artificial latency to test timeout handling. Playwright achieves the same through route.abort() for network errors and a delayed route.fulfill() response for latency tests. A complete test scenario typically covers four cases: success, a business error such as a declined payment, a technical server error such as 500, and a timeout, each backed by its own clearly named fixture.


// cypress/e2e/checkout-payment-errors.cy.js

it('shows a clear error message when the payment gateway times out', () => {
  cy.intercept('POST', '**/v2/authorize', (req) => {
    req.reply({ delay: 15000, statusCode: 200, body: {} });
  }).as('authorizeTimeout');

  cy.visit('/checkout');
  cy.get('[data-testid="place-order"]').click();

  // The app should surface a timeout message before Cypress' own default timeout
  cy.get('[data-testid="payment-error"]', { timeout: 12000 })
    .should('contain.text', 'The payment provider is not responding');
});

it('shows a decline message on a rejected payment', () => {
  cy.intercept('POST', '**/v2/authorize', {
    statusCode: 402,
    body: { status: 'declined', reason: 'insufficient_funds' },
  }).as('authorizeDeclined');

  cy.visit('/checkout');
  cy.get('[data-testid="place-order"]').click();
  cy.wait('@authorizeDeclined');
  cy.get('[data-testid="payment-error"]').should('contain.text', 'declined');
});

7. Contract testing: validating mocks against the real API

Mocking solves the problem of risky real calls, but it creates a new risk: a mock can stay green indefinitely while the real API has long since changed. If a payment provider renames a field or introduces a new required parameter, a fully mocked suite notices nothing until the integration actually breaks in production. Contract testing closes that gap by regularly validating the mocks' assumptions against the real API, independently of the fast, mocked main suite.

A pragmatic starting point without a full consumer driven contract framework like Pact is a separate, rarely running job that validates the current fixtures or mappings against a JSON schema derived from a real sandbox response. Running this job weekly instead of on every commit keeps the main suite fast and stable, while schema drift still surfaces within days rather than showing up as a production incident.

8. Test data management: versioning and maintaining fixtures

Fixtures belong in the repository as plain text files, JSON or YAML, under version control, usually in a dedicated folder such as cypress/fixtures/payment/ or tests/mocks/shipping/. A consistent naming convention per scenario, authorize-success.json, authorize-declined.json, authorize-timeout.json, makes it obvious at a glance which case uses which fixture without opening the file. Fixture changes should go through the same code review process as production code, because an unnoticed, incorrectly adjusted fixture can silently make an entire test case meaningless.

A common trap is the single, generic success fixture reused for almost every test, which swallows edge cases like different currencies or amounts. Smaller, deliberately parameterized fixtures or a factory function that overrides a base fixture with test specific values at runtime work better. That keeps the file count manageable while every test still checks exactly the values relevant to its specific case.

9. When to test live, when to mock

Mocking should not mean an application never runs against the real API again. A small, deliberately limited set of live smoke tests against the sandbox, separate from the fast main suite and run infrequently, remains important for catching real integration problems that no mock could ever surface. The following overview shows how this decision typically plays out in practice.

Test case Live API call Mock approach Recommendation
Payment happy path on every PR Real sandbox call on every push Recorded fixture via MSW/WireMock Mock, check the sandbox nightly only
Timeout and error handling Barely reproducible against a real sandbox cy.intercept with forceNetworkError/delay Always mock
Shipping rates from multiple carriers Rate limits under parallel CI WireMock stub per carrier response Mock, verify changes via contract test
Catching payment API schema drift Manual, often noticed too late Contract test against the real sandbox Check live weekly
First integration of a new provider Develop directly against the sandbox No fixtures exist yet Live briefly, then record and mock

Mironsoft

E2E test automation and mock server setups for Magento and Hyva stores

An E2E suite that does not depend on someone else's infrastructure?

We build resilient mock server setups with MSW and WireMock, versioned fixture libraries, and contract testing pipelines, so your payment and shipping integrations are reliably tested without risking real sandbox calls on every run.

MSW/WireMock Setup

Integrate mock servers for payment and shipping APIs cleanly into CI

Fixture Library

Version recorded responses and maintain them for error scenarios

Contract Testing

Catch schema drift between mocks and the real API early

10. Summary

Real calls to payment providers, shipping carriers, and other external APIs on every test run are expensive, slow, and prone to failures that have nothing to do with your own code. MSW mocks requests at the network level for client side calls, WireMock style server mocks cover server side backend to API communication, and cy.intercept() or page.route() are enough for many cases directly inside the test framework. Recorded fixtures instead of handwritten responses keep mocks as close to reality as possible.

The biggest payoff lies in error and timeout scenarios that are nearly impossible to reliably force against a real sandbox but become deterministic and reproducible with mocking. To keep mocks from silently drifting away from reality, teams need contract testing and a small, deliberate set of real sandbox smoke tests on top. Getting that balance right produces an E2E suite that stays fast, stable, and still realistic.

Mocking External APIs, The Essentials at a Glance

Risks of real calls

Cost, rate limits, side effects, and flakiness from third party infrastructure on every test run.

Mock strategies

MSW for client side, WireMock for server side calls, cy.intercept/page.route for fast in framework cases.

Error scenarios

Simulate timeouts, 500s, and declines deterministically instead of hoping for them against the sandbox.

Contract testing

Regularly validate fixtures against the real API so mocks do not silently go stale.

11. FAQ: Mocking External APIs in Tests

1Why mock external APIs in E2E tests?
Real calls cost time, are subject to rate limits, and can fail because of third party outages without an actual bug in your code. Mocking decouples test reliability from external uptime.
2MSW vs. WireMock, what's the difference?
MSW intercepts requests in the browser or Node.js, suited for client side calls. WireMock is its own server process for server side backend to API calls.
3How do you record real responses as fixtures?
A proxy recording mode or an HAR mechanism like routeFromHAR forwards requests to the real sandbox, captures the response, and stores it for later replay.
4How do you simulate a timeout?
In Cypress via cy.intercept with delay in req.reply, in Playwright via a delayed route.fulfill response. Both deterministically force a long response time.
5What is contract testing?
Regular checks that mock assumptions still match the real API, for example via schema validation. Prevents a mocked suite from staying green while the integration breaks.
6Do you still need real sandbox tests?
Yes, a small, infrequently run set of live smoke tests remains important to catch real integration problems, without slowing down the fast main suite.
7How do you keep fixtures current?
Fixtures versioned in the repository, same code review as production code, regular re-recording against the sandbox, and supplementary contract testing.
8Risk of an overly generic fixture?
A single success fixture reused everywhere swallows edge cases like currencies or amounts. Smaller, parameterized fixtures represent real variation more accurately.
9Difference from cy.intercept and page.route?
cy.intercept and page.route only work inside the running test framework for browser requests. MSW and WireMock run independently and also cover server side calls.
10Do you still need a sandbox account?
Yes, for the initial fixture recording, occasional contract tests, and new integrations. For day to day testing, mocks handle the majority of calls.