Introduction
Cypress has become one of the most popular end-to-end testing tools for modern web applications. Its architecture and developer-friendly tooling remove many of the traditional pain points of UI testing.
However, writing good tests still requires discipline. In this post, we’ll explore the best practices that will help you keep your Cypress suite fast and reliable.
1) Use Robust Selectors
One of the most common causes of flaky tests is brittle selectors. If you rely on CSS classes or deep DOM structures, your tests will break as soon as the UI changes.
Bad Practice:
cy.get('.btn-primary.submit-btn').click()
Best Practice: Use dedicated data attributes for testing.
cy.get('[data-cy="submit-btn"]').click()
This clearly separates testing concerns from styling and behavior.
2) Avoid Hard-Coded Wait Times
Never use cy.wait(5000). It makes your tests slow and fragile. If a page takes 6 seconds to load, the test fails; if it takes 1 second, you’ve wasted 4 seconds.
Best Practice: Rely on Cypress's built-in command retries or wait for specific network requests.
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers') // Only waits as long as necessary
3) Keep Tests Independent
Your tests should be able to run in any order. If Test B depends on a state created in Test A, a failure in Test A will cascade into Test B.
Best Practice:
Ensure each test sets up its own state. Use beforeEach to clear cookies, reset local storage, or seed the database.
4) Don't Test External Sites
Cypress is designed to test your application. Avoid navigating to Google, Facebook, or third-party payment providers in your tests.
Best Practice:
If you need to verify an integration, use cy.request() to interact with the API directly or mock the external responses entirely.
5) Use Custom Commands for Repetitive Tasks
If you find yourself writing the same login logic in every test file, it’s time for a custom command.
Best Practice:
Add commands to cypress/support/commands.js:
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login')
cy.get('[data-cy="email"]').type(email)
cy.get('[data-cy="password"]').type(password)
cy.get('[data-cy="login-btn"]').click()
})
6) Clean Up Your Scope
Don't try to test everything in one giant spec file.
Best Practice:
Break your tests down by feature or user flow (e.g., auth.cy.js, cart.cy.js, profile.cy.js). This makes debugging easier and allows for better parallel execution.
7) Assert State, Not Side Effects
Instead of checking if a function was called, check if the UI reflects the expected change.
Best Practice:
cy.get('[data-cy="delete-item"]').click()
cy.get('[data-cy="item-list"]').should('not.contain', 'Important Item')
8) Maintain Your Suite
Treat your test code with the same respect as your application code. As the application evolves, tests must evolve with it.
Make time to:
- refactor outdated tests
- remove redundant coverage
- improve slow or unstable areas
Regular maintenance keeps your suite fast, trustworthy, and aligned with the product.
9) Follow Clear Naming Conventions
Consistent naming improves navigation and collaboration.
Define conventions early, for example:
- use descriptive file names
- keep test titles explicit and scenario-focused
- follow a consistent casing style across the project
Clear naming reduces cognitive load and helps new team members understand the test suite quickly.
Conclusion
Cypress provides a strong foundation for modern test automation, but long-term success depends on how tests are written and maintained.
By prioritizing clarity, stability, and usefulness over sheer test count, you can build a Cypress test suite that scales with your application and continues to deliver real value over time.
Comments