Back to stories
<QA/>

Cypress Basics (Part 3): Best Practices and Running in CI

Share by

Cypress Basics (Part 3): Best Practices and Running in CI

After writing your first Cypress tests, the next step is to keep them maintainable and reliable: how to organize specs, debug failures, run in CI, and what to avoid. This post wraps up the Cypress basics series with practical patterns and anti-patterns.


Organizing your specs

One feature or flow per file

Group tests by feature or user flow, not by technical detail. For example:

  • login.cy.js — login, logout, forgot password
  • dashboard.cy.js — dashboard load, widgets, navigation
  • checkout.cy.js — cart, checkout steps, confirmation

That makes it easy to find and run only the tests that matter when something breaks.

Use beforeEach for setup

Visit the page (or log in) once per test so each test starts from a known state:

describe('Dashboard', () => {
  beforeEach(() => {
    cy.visit('/')
    cy.login('user@example.com', 'password') // custom command
  })

  it('shows welcome message', () => { ... })
  it('navigates to settings', () => { ... })
})

Avoid sharing mutable state between tests; isolation reduces flakiness.

Custom commands for repeated flows

If you log in in many specs, add a custom command in cypress/support/commands.js:

Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login')
  cy.get('[data-testid="email"]').type(email)
  cy.get('[data-testid="password"]').type(password)
  cy.get('button[type="submit"]').click()
  cy.url().should('include', '/dashboard')
})

Then use cy.login('user@example.com', 'password') in any spec. Keeps tests short and consistent.


Debugging failures

Use the Test Runner

Run npx cypress open and click a failing spec. Cypress shows each command; click a step to see the DOM and console at that moment. That "time-travel" view usually makes it clear why an assertion failed.

Screenshots and videos

On failure, Cypress can capture a screenshot and record a video (in cypress/videos/). Enable in config:

// cypress.config.js
module.exports = {
  screenshotOnRunFailure: true,
  video: true,
}

In CI, upload these artifacts so you can see what happened without rerunning locally.

cy.pause() and cy.debug()

  • cy.pause() stops the test so you can step through in the Test Runner.
  • cy.debug() stops and opens the browser devtools so you can inspect. Remove or comment them out before committing.

Running in CI

Headless run

npx cypress run

Use this in your CI script (e.g. in a test:e2e npm script). Cypress runs in headless Electron by default; you can switch to Chrome or Firefox with --browser chrome.

Config for CI

Set a baseUrl so tests use a real or stubbed app:

// cypress.config.js
module.exports = {
  baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000',
  video: true,
  screenshotOnRunFailure: true,
}

In CI, set CYPRESS_BASE_URL to your deployed preview or staging URL (or a local server started in the same job).

Start the app before tests

Your CI pipeline should start the app, wait until it's up, then run Cypress. Example with a Node app:

npm run start &   # or use a script that starts the server
npx wait-on http://localhost:3000
npx cypress run

Use your CI's job steps or a script to start the server, wait for health, then run cypress run.


Anti-patterns to avoid

Don't use cy.wait(ms)

Avoid fixed delays like cy.wait(3000). They slow tests and still fail when the app is slow. Prefer:

  • Assertions that retry: cy.get('.loaded').should('be.visible')
  • Waiting on network: cy.intercept('GET', '/api/data').as('data'); cy.visit('/'); cy.wait('@data')

Don't assert on too many things in one test

One test should verify one behavior. If a test has many unrelated assertions, split it so failures are easier to understand and fix.

Don't depend on test order

Each test should be able to run alone. Use beforeEach to set up state; don't assume test B runs after test A.

Don't use production-only data

If your tests depend on a specific user or record in the DB, they break when data changes. Use fixtures, seed data, or API stubs so tests control the data.


Summary

  • Organize specs by feature or flow; use beforeEach and custom commands (cy.login) for shared setup.
  • Debug with the Test Runner's time-travel view, screenshots, and videos; use cy.pause() or cy.debug() when needed.
  • CI: Run npx cypress run, set baseUrl via env, start the app before tests, and upload videos/screenshots on failure.
  • Avoid fixed cy.wait(ms), oversized tests, test-order dependencies, and production-only data.

With these basics—installation, writing tests, and best practices—you can add and maintain a solid Cypress E2E suite. For more, see the Cypress docs and the rest of the blog for related topics on test design and automation.