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.

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.
cy.intercept() works at the network level: it catches requests that match a URL pattern and can replace their responses:
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.
Once the alias exists, the test waits for the request and can inspect its details:
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.
Error responses are often impossible to reproduce without taking down the server. With a stub, you can force them anytime:
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.
Fallbacks can also be tested: simulate a dead API and make sure the UI shows the right message:
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.
Stubbing frees tests from real data that can change. You can test edge states with a controlled dataset:
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.
For large datasets, combine with the fixtures from episode 6:
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.
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:
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.
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.
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.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.