Cypress: patterns that work โ
With the first steps you write a test; with these patterns you maintain a suite. They're what separates a stable suite from one that "fails sometimes".
Custom commands: log in once โ
Repeated flows become your own commands in support/commands.ts. The universal case is login, and with cy.session the session is cached across tests: the real login happens once, not in every test.
Cypress.Commands.add('login', (user = 'sales-agent') => {
cy.session(user, () => {
cy.visit('/login');
cy.get('[data-cy=email]').type(Cypress.env(`${user}_email`));
cy.get('[data-cy=password]').type(Cypress.env(`${user}_password`), { log: false });
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/dashboard');
});
});beforeEach(() => cy.login());This alone can shave whole minutes off a slow suite.
cy.intercept: the network superpower โ
cy.intercept observes or replaces the requests the application makes. Its three uses:
1. Waiting for the network instead of for time (goodbye cy.wait(3000)):
cy.intercept('GET', '/api/catalog/products').as('getCatalog');
cy.visit('/catalog');
cy.wait('@getCatalog'); // waits for exactly that response
cy.contains('Fiber 1 Gbps');2. Stubbing responses with a fixture (the frontend gets tested without depending on the backend):
cy.intercept('GET', '/api/service-orders*', { fixture: 'orders-in-provisioning.json' });3. Forcing the hard cases that are costly to provoke in a real environment:
cy.intercept('POST', '/api/service-orders', { statusCode: 500 }).as('orderFails');
cy.get('[data-cy=confirm-order]').click();
cy.contains('The order could not be created'); // does the UI handle the error well?The balancing rule: stubbing the network tests the frontend in isolation; critical flows (actually ordering a service) also need tests against the real backend. Both kinds, deliberately โ it's the pyramid applied inside E2E.
Organization that scales โ
- One spec per business flow (
ordering.cy.ts,catalog.cy.ts), not per page. - State via API, verification via UI: the starting customer and order are created through the API (faster and more stable), and the UI is used for what's actually under test.
- Page objects? In Cypress the classic POM weighs less than in Selenium:
data-cy+ custom commands cover most reuse. For complex screens (a tariff configurator), a page object is still reasonable. - Retries in CI:
retries: { runMode: 2 }in the config gives margin against environment instability โ but a test that needs retries is always asking for an investigation, not a third attempt.
In the pipeline โ
npx cypress run --browser chrome --spec "cypress/e2e/ordering/**"- Videos and failure screenshots land in
cypress/videosandcypress/screenshots: publish them as artifacts always, including on red. - The long suite gets split across containers by specs: the criteria live in parallelization and sharding.
Common mistakes โ
- Stubbing everything. A suite that only talks to mocks passes even when the backend is broken; keep a real E2E subset for critical business flows.
- Not using
cy.sessionand paying a full login per test. - Weak assertions (
should('exist')where content needed checking): the test passes, so does the bug. - One giant spec: impossible to parallelize and to read.
Key idea
The three multipliers of a healthy Cypress suite: cached session with cy.session, controlled network with cy.intercept, and state prepared via API. Everything else is writing good test cases.