Learn Cypress - API Testing & Network Stubbing
Episode 12 of 23

Learn Cypress - API Testing & Network Stubbing

This episode covers using cy.intercept to stub APIs, testing error responses, retries, and fallback flows, validating the UI with mocked backend data, and full-stack testing with real backend integration.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The network is the second biggest source of flakiness after timing. Episode 12 focuses on cy.intercept(): stubbing APIs, testing error responses, retries, and fallbacks, validating the UI with mock data, and full-stack testing with a real backend.

Mastering stubbing means you can test every side of the UI without depending on backend availability — and force the application to face scenarios that rarely happen in production.

Using cy.intercept to Stub APIs

Capturing and Replacing Responses

cy.intercept() works at the network level: it catches requests that match a URL pattern and can replace their responses:

JSStubbing a GET response
cy.intercept("GET", "/api/produk", {
  statusCode: 200,
  body: [
    { id: 1, nama: "Kopi", harga: 45000 },
    { id: 2, nama: "Teh", harga: 20000 },
  ],
}).as("produk");

cy.intercept("GET", "/api/produk", ...) intercepts GET requests to that pattern and returns mock data. .as("produk") gives it an alias so the test can wait on its event.

Waiting for a Captured Request

Once the alias exists, the test waits for the request and can inspect its details:

JSWaiting for and inspecting a request
cy.visit("/katalog");
cy.wait("@produk").its("response.statusCode").should("eq", 200);
cy.get("[data-cy=daftar-produk]").children().should("have.length", 2);

cy.wait("@produk") waits until the matching request happens. cy.get("[data-cy=daftar-produk]") then verifies the UI renders the data sent by the stub.

Testing Error Responses, Retries, and Fallbacks

Forcing Errors and Retries

Error responses are often impossible to reproduce without taking down the server. With a stub, you can force them anytime:

JSStubbing an error then a success
let gagal = true;
 
cy.intercept("GET", "/api/produk", (req) => {
  if (gagal) {
    req.reply({ statusCode: 500, body: { error: "internal" } });
    gagal = false;
  } else {
    req.continue();
  }
}).as("produkRetry");

req.reply({ statusCode: 500, ... }) sends an error on the first attempt. req.continue() forwards the request to the real server on the second attempt — perfect for testing the application's retry mechanism without complicated scenarios.

Fallback Flows

Fallbacks can also be tested: simulate a dead API and make sure the UI shows the right message:

JSTesting an offline fallback
cy.intercept("GET", "/api/produk", (req) => {
  req.reply({ statusCode: 503, body: {} });
}).as("produkDown");
 
cy.visit("/katalog");
cy.wait("@produkDown");
cy.contains("Sedang tidak tersedia").should("be.visible");

cy.wait("@produkDown") waits for the stubbed 503 response. The assertion cy.contains("Sedang tidak tersedia") ensures the UI shows a fallback message instead of an empty page.

Validating the UI with Mocked Backend Data

Scenarios with Varied Data

Stubbing frees tests from real data that can change. You can test edge states with a controlled dataset:

JSValidating the UI with mock data
it("menampilkan harga diskon", () => {
  cy.intercept("GET", "/api/produk/1", {
    body: { id: 1, nama: "Kopi", harga: 45000, diskon: 0.2 },
  });
 
  cy.visit("/produk/1");
  cy.get("[data-cy=harga-final]").should("have.text", "Rp36.000");
});

cy.intercept("GET", "/api/produk/1", { body: ... }) specifies per-product data. The test validates the price calculation shown in the UI, without needing a backend that actually stores that product.

Combining with Fixtures

For large datasets, combine with the fixtures from episode 6:

JSStubbing with a fixture
cy.intercept("GET", "/api/produk", { fixture: "api/produk.json" }).as("katalog");

cy.intercept("GET", "/api/produk", { fixture: "api/produk.json" }) loads the response from a fixture file. This pattern keeps data easy to manage and reuse across tests.

Full-Stack Testing with Backend Integration

When Not to Stub

Stubbing is not everything. When the goal of a test is to verify the frontend and backend working together, you need to run both. Use cy.request() to set up real data:

JSSetting up data via a request
beforeEach(() => {
  cy.request("POST", "/api/seed", { jumlah: 5 });
  cy.visit("/katalog");
});

cy.request("POST", "/api/seed", ...) calls the backend API directly to set up state. This avoids slow UI interactions for things that are only setup, while the core test still uses the real backend.

Choosing the Right Strategy

The rule: stub to test the UI in isolation and for edge scenarios; use the real backend to test integration. Mixing both in one suite is common practice — the key is documenting which is which so the team is not confused when a test fails.

Warning

Overly broad stubs can hide real bugs in both the frontend and the backend. Make sure the URL patterns in cy.intercept() are specific, and include some real integration tests without stubs in every important suite.

Closing

Episode 12 made the network a controlled area: cy.intercept() for stubbing APIs, testing errors, retries, and fallbacks, validating the UI with mock data or fixtures, and full-stack testing with a real backend via cy.request().

Key takeaways:

  • cy.intercept(pola, respons) stubs requests; .as() gives them an alias.
  • req.reply() and req.continue() control responses per attempt.
  • Errors, retries, and fallbacks can be tested without taking down the server.
  • Stubs are best for UI isolation; the real backend for integration.
  • Use cy.request() for data setup without slow UI interactions.

In the next episode, episode 13, we will cover security and test stability — isolating test data and environments, running tests with secure credentials, avoiding flakiness with stable selectors, and monitoring and maintaining reliability.

Learn Cypress - API Testing & Network Stubbing | Learn Cypress