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 passworddashboard.cy.js— dashboard load, widgets, navigationcheckout.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
beforeEachand custom commands (cy.login) for shared setup. - Debug with the Test Runner's time-travel view, screenshots, and videos; use
cy.pause()orcy.debug()when needed. - CI: Run
npx cypress run, setbaseUrlvia 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.