End-to-End Testing with Cypress: A Practical Guide
Install Cypress, write reliable end-to-end tests, choose between E2E and component coverage, and run your browser tests in CI.
Cypress end-to-end (E2E) tests exercise your application in a real browser, from the interface through the back end and, where relevant, third-party integrations. To get started, add Cypress as a development dependency, start your app in a known test environment, then write a test that establishes state, performs a user-like action, and checks an outcome that matters.
This guide covers installation, a runnable workflow test, test isolation, retries, E2E versus component tests, and continuous integration (CI). Cypress’s documentation is the source for the Cypress-specific recommendations below; check its current requirements and provider instructions when setting up a project.
1. What Cypress E2E tests verify
An E2E test exercises the application through a browser and can cross application layers: the UI, back-end services, persisted data, and integrations. Use E2E tests for important complete journeys, such as signing in, completing a purchase, or submitting a form and verifying the resulting state. They can also serve as smoke checks before deployment.
Because these tests cover more of the system, they usually need more environment setup and can take more effort to maintain than isolated tests. A failure may come from the browser workflow, application code, a service, test data, or infrastructure. Keep the test environment stable and known so results are easier to interpret. Cypress recommends starting the application server for local development rather than starting it inside Cypress test scripts. Cypress: Testing Your App.
2. Install Cypress and open the test runner
Add Cypress to your project’s development dependencies using its package manager. Run the command from the project root:
# npm
npm install --save-dev cypress
# Yarn
yarn add --dev cypress
# pnpm
pnpm add --save-dev cypress
# Bun
bun add --dev cypress
Then open Cypress:
npx cypress open
The first-run app guides you through choosing E2E or component testing and creating starter configuration and spec files. Cypress’s default E2E spec location is cypress/e2e; configuration can change it. Check the current Cypress installation guide for platform and runtime requirements, which can change.
For a headless run, use:
npx cypress run
Keep your application server as a separate process during local development. A typical workflow is to start the app with the project’s development command in one terminal, wait until it is ready, and run Cypress from another.
3. Write a focused workflow test
A useful test follows a simple sequence: establish the starting state, take an action, and assert the resulting state. Cypress’s first-test guidance similarly walks through visiting a page, querying an element, interacting with it, and checking the result. The assertion should describe a user-visible or otherwise meaningful outcome, not merely that a command executed.
For example, suppose the app has a login page at /login, fields with stable data-cy attributes, and a successful login redirects to /dashboard. Save this as cypress/e2e/login.cy.js:
describe('login', () => {
it('signs in and opens the dashboard', () => {
cy.visit('/login')
cy.get('[data-cy=email]').type('reader@example.com')
cy.get('[data-cy=password]').type('correct-test-password')
cy.get('[data-cy=login-submit]').click()
cy.location('pathname').should('eq', '/dashboard')
cy.get('[data-cy=welcome]').should('be.visible')
})
})
Replace the example selectors, route, and credentials with values from your application. Provide the test account through a safe test-data mechanism appropriate to your environment; do not commit real user credentials. Selectors intended for tests, such as data-cy, tend to be less coupled to styling and layout than selectors based on CSS classes.
Set the application’s base URL in Cypress configuration so cy.visit('/login') resolves against your running test server. In current Cypress projects, this is configured in cypress.config.js:
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
},
})
Adjust the port and configuration file format to match your project. Cypress uses familiar Mocha-style describe and it blocks with Chai assertions. Support files load before specs and are intended for shared setup and custom commands; use them for genuinely reusable behavior rather than hiding the steps of an individual test. See Cypress: Writing and Organizing Tests and Cypress: Writing Your First End-to-End Test.
4. Make tests independent and reliable
Each test should be runnable on its own. Avoid relying on another test to create a user, leave the browser on a particular page, or populate shared state. Cypress enables E2E test isolation by default and cleans browser context between tests. This helps prevent order-dependent failures, but it does not automatically reset server-side data such as database records.
- Arrange required data explicitly for each test or through a controlled fixture/setup step.
- Make cleanup or data reset predictable, especially when tests share an account or database.
- Assert observable outcomes, and wait for the condition that matters rather than relying on arbitrary delays.
- Keep the test environment and external dependencies stable where possible.
Retries are not enabled by default. Cypress identifies animations, API calls, server or database availability, resource dependencies, and network issues as possible sources of unpredictable failures. Retries can help expose or manage intermittent failures, but repeated attempts can also mask a defect or unstable environment. Investigate the underlying cause instead of treating a passing retry as proof that the test is reliable. Consult Cypress: Test Retries for configuration and reporting details.
5. Choose E2E or component tests
| Test type | What it exercises | Best suited to | Tradeoff |
|---|---|---|---|
| E2E | A complete browser workflow across application layers | Critical journeys, integration behavior, persisted results, smoke checks | More environment setup and maintenance |
| Component | A component mounted and tested in isolation | Focused interaction and rendering scenarios with simpler setup | A passing component test does not establish that the full app works together |
Use the test type that answers the question you have. Component tests give focused feedback about a component; E2E tests show whether important parts of the assembled application work together in a browser. They complement each other. Cypress describes these different scopes in its testing types guide.
6. Run Cypress in CI
CI needs a running application before Cypress starts. Starting the server in the background and immediately launching tests creates a race: Cypress may attempt to visit the app before it is listening. Wait for a readiness check that confirms the server can serve requests. Cypress documents CI setups for GitHub Actions, CircleCI, GitLab CI, Jenkins, and AWS CodeBuild; provider configuration can evolve, so use the current instructions for your runner.
A minimal GitHub Actions example using Cypress’s official action:
name: e2e
on: [push, pull_request]
jobs:
cypress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- uses: cypress-io/github-action@v6
with:
start: npm run start:test
wait-on: 'http://localhost:3000'
wait-on-timeout: 120
Adapt the Node version, action versions, start script, and readiness URL to your project and current provider guidance. The application start command should run the app in the test configuration. A readiness check is preferable to a fixed sleep because startup time can vary. See Cypress: Continuous Integration and the official Cypress GitHub Action.
7. Troubleshoot common failures
| Symptom | Likely cause | What to do |
|---|---|---|
cy.visit() cannot reach the application |
The server is not running, the base URL or port is wrong, or CI started Cypress before the app was ready. | Start the app separately for local runs; verify the configured URL and add a CI readiness check. |
| A test passes alone but fails in the suite | It depends on browser or server-side state left by another test, or tests compete over shared data. | Make setup explicit, reset shared data as needed, and run the failing test independently to locate the dependency. |
| Element query times out | The selector does not match, the element is not rendered yet, or the page is in an unexpected state. | Check the selector and route, inspect the rendered page, and assert the state that should make the element appear. |
| Intermittent failures around animation or network activity | Timing, resource availability, or environment instability makes the outcome unpredictable. | Wait on the meaningful condition, stabilize the dependency or test data, and investigate before enabling retries. |
| CI fails while local runs pass | CI may have different configuration, missing test data, a different startup sequence, or an app readiness race. | Compare environment settings, confirm the test server’s health check, and inspect the CI run output for the earliest failure. |
8. Performance, reliability, and cost
E2E tests cover more of the application, so their environment and dependencies matter. Keep the suite focused on user journeys where integration coverage is valuable; use component tests for isolated behavior that does not need a full browser journey. Reuse stable setup patterns, but keep each test’s required state explicit. Parallel execution may be available in a CI setup, but shared accounts and mutable test data need isolation before parallel runs are safe.
Reliability comes from deterministic setup, independent tests, meaningful assertions, and a server readiness check. Retries may be useful as a diagnostic or configured policy, but track which tests need them and fix recurring causes. Cost depends on your own runner capacity and the infrastructure and services your test environment uses; the Cypress documentation cited here does not establish a universal runtime or cost benchmark.
9. Capture screenshots of pages in a test workflow
Cypress is for exercising an application workflow in a browser. If your task is instead to capture a page image or PDF for documentation, review, or an agent workflow, a screenshot API can handle the capture separately. ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its options include full-page capture, CSS-selector element capture, device and viewport settings, PDF output, custom waits, and custom CSS or JavaScript. See ScreenshotNeo and its API documentation.
10. FAQ
Does an E2E test need a real browser?
Cypress E2E tests exercise the application in a browser, with user-like actions and checks across the running application.
Should every test be an E2E test?
No. Use E2E coverage for complete workflows and integration behavior; use component tests for focused scenarios. Choose based on what needs verification.
Do Cypress tests retry automatically?
No. Retries are opt-in. A retry should not replace investigating intermittent failures.
Can I start the app from a Cypress test?
Cypress recommends starting the application server outside the test scripts so the environment is established before the browser workflow begins.
Or skip the browser setup
For a standalone website capture, ScreenshotNeo takes a screenshot with one GET request. The example saves the returned image as WebP; replace the URL and provide your API key. See the ScreenshotNeo API docs for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
- Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Response headers report the page verdict and billing status.
- An MCP server lets Claude, Cursor, and other MCP clients use screenshot, page-info, and PDF tools.
- The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.


