Understanding End-to-End Testing with Cypress
Learn what E2E tests cover, write your first Cypress scenario, and avoid brittle automated tests.
Understanding End-to-End Testing with Cypress
An application can pass all its unit tests and still fail in a way users immediately notice: a button stops responding, a form cannot be submitted, or navigation leads to an error page.
End-to-end (E2E) testing checks a complete user journey, from the interface to the visible outcome. Cypress is a tool for automating these scenarios in a browser.
1. Where do E2E tests fit?
Different testing levels answer different questions:
- Unit tests: does a function or component work correctly in isolation?
- Integration tests: do multiple components interact as intended?
- E2E tests: can a user complete an important workflow in the application?
Consider a contact form. A unit test might validate the email address. An E2E test checks whether a visitor can fill out the form, submit it, and see a confirmation message.
E2E tests provide confidence, but they also cost more to run and maintain. The goal is not to test everything through the UI: focus on important business workflows.
2. Write your first Cypress scenario
Consider a fictional login page. We want to verify that a user can sign in with valid credentials:
describe('Login', () => {
it('allows a user to sign in', () => {
cy.visit('/login');
cy.get('[data-cy="email"]').type('[email protected]');
cy.get('[data-cy="password"]').type('test-password');
cy.get('[data-cy="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('[data-cy="welcome"]').should('be.visible');
});
});This test describes the expected behavior: open the page, enter data, submit the form, and confirm navigation and the resulting UI.
The credentials above are fictional. Real projects should use dedicated test accounts and keep secrets protected.
3. Use stable selectors
A selector tied to layout details can break after a small design change:
cy.get('.card > div:nth-child(2) button').click();A test-specific attribute is often more robust:
<button data-cy="submit">Sign in</button>cy.get('[data-cy="submit"]').click();The aim is to decouple tests from visual structure. Accessible roles and names are also useful when working with tooling that supports role-based queries.
4. Avoid arbitrary waits
This is a fragile pattern:
cy.wait(3000);
cy.get('[data-cy="result"]').should('be.visible');It adds three seconds even when the page is ready immediately. If the server needs four seconds, it can still fail.
Cypress automatically retries many assertions. Prefer waiting for an observable state or a relevant request:
cy.intercept('GET', '/api/exams*').as('getExams');
cy.visit('/exams');
cy.wait('@getExams');
cy.get('[data-cy="exam-list"]').should('be.visible');This approach avoids unnecessary waiting and communicates the intent of the test.
5. Control test data
A reliable test starts from a predictable state. If it silently assumes that a certain user or exam already exists, it may pass today and fail tomorrow.
Depending on the system, you can:
- Create the required data before the scenario.
- Reset the testing environment.
- Intercept selected network responses to isolate UI behavior.
- Use a dedicated test database.
It's important to distinguish tests that exercise the real journey through the backend from tests that simulate a response to focus on frontend behavior.
6. Cover failure cases too
The happy path is not enough. For a form, you might also test validation errors:
it('shows an error when the form is incomplete', () => {
cy.visit('/contact');
cy.get('[data-cy="submit"]').click();
cy.get('[data-cy="validation-error"]').should('be.visible');
});Other useful cases include permissions, server errors, slow responses, and repeated submissions.
7. Run E2E tests in CI
Once your tests are stable, run them in a continuous integration pipeline. A failure becomes a signal to investigate before releasing a change.
To keep pipelines fast, distinguish essential tests run on every change from a broader suite run periodically or before deployment.
Final thoughts
A good E2E test does not try to reproduce every possible click. It describes an important user behavior, uses stable selectors, controls its data, and checks observable outcomes. Cypress provides the automation tools; the quality of the test depends on the scenarios you choose.