ScreenshotNeo

BlogGuides

Modern Web Testing with Cypress: A Practical Guide

Choose the right Cypress test layers, write useful real and stubbed tests, and make browser coverage reliable in CI.

By the ScreenshotNeo team4 October 202612 min read

Cypress is a browser testing platform you install in your project and run locally or in continuous integration (CI). Use end-to-end (E2E) tests to check important user journeys through the application and backend, component tests to exercise focused UI behavior in a real browser, API tests to check endpoints directly, and accessibility checks to find some barriers in the UI. A reliable suite combines these layers, uses real server responses where integration confidence matters, and stubs responses when a controlled UI scenario is the goal.

This guide walks through installation, runnable examples, test strategy, network stubbing, browser coverage, CI readiness, common failures, and practical cost and reliability tradeoffs. Cypress’s testing types overview describes the scope and tradeoffs of each layer.

1. Install Cypress and open a project

Install Cypress as a development dependency from the project root. Choose one package manager and use its matching command:

# npm
npm install --save-dev cypress
npx cypress open

# Yarn
yarn add --dev cypress
yarn cypress open

# pnpm
pnpm add --save-dev cypress
pnpm cypress open

# Bun
bun add --dev cypress
bunx cypress open

The first launch opens the Cypress App, where you can choose E2E or component testing and create the initial configuration and spec files. Follow the current installation guide and system requirements; supported operating systems, Node.js versions, and browser requirements change over time. Commit the package manifest and lockfile so local and CI installs resolve the same dependency tree.

For a quick start, let Cypress generate configuration in the App. For a committed E2E setup, a minimal configuration can look like this:

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://127.0.0.1:3000',
    specPattern: 'cypress/e2e/**/*.cy.js',
  },
})

Then create a first spec and run it in the interactive App or from the command line:

// cypress/e2e/home.cy.js
describe('home page', () => {
  it('shows the page heading', () => {
    cy.visit('/')
    cy.get('h1').should('be.visible')
  })
})
npx cypress open --e2e
npx cypress run --browser chrome

The app must be running before this test visits it. Set baseUrl to the address where your local or CI server listens. See the official configuration reference for options such as spec patterns, support files, retries, and test isolation.

2. Choose the test layer that answers your question

Layer What it exercises Good fit Main limitation
E2E The application in a browser, commonly including the backend Sign-in, checkout, persistence across screens, pre-release smoke paths Needs a running app and often prepared backend state; broader paths can be slower and more failure-prone
Component A mounted component in a real browser Form behavior, date picker interactions, design-system components, focused UI states Does not prove the whole application and backend integrate correctly
API An HTTP endpoint and its response Validation, authorization, pagination, CRUD contracts, test-data setup Does not exercise the UI
Accessibility Accessibility properties of a page or component, sometimes layered onto another test Labels, semantic structure, and automated scans for known rule violations Automated checks cannot establish that an interface is fully accessible

A practical suite puts fast, focused checks near the component or endpoint and reserves E2E tests for important user paths that cross application layers. This avoids making every test prove the same thing. Cypress documents these distinctions in its test types guide.

Example: a meaningful E2E path

Use stable selectors that are part of your testing contract, such as data-cy, rather than selectors tied to styling or incidental DOM structure.

describe('todo flow', () => {
  it('adds an item', () => {
    cy.visit('/')
    cy.get('[data-cy=new-todo]').type('write tests{enter}')
    cy.get('[data-cy=todos]').should('contain', 'write tests')
  })
})

Keep each test’s setup explicit and its assertions about user-visible outcomes. When tests depend on shared state, make that state deterministic through an API setup step, a seeded test database, or a controlled stub. Cypress’s configuration reference explains test isolation settings; avoid relying on state left behind by a previous test.

Example: API check and data setup

cy.request() sends an HTTP request directly from a Cypress test. It is useful for asserting an API contract and for preparing data without repeating a UI flow in every spec.

describe('users API', () => {
  it('returns a user', () => {
    cy.request('/api/users/1').then((response) => {
      expect(response.status).to.equal(200)
      expect(response.body).to.have.property('id', 1)
      expect(response.body).to.have.property('email')
    })
  })
})

This assumes the configured app server exposes /api/users/1. For an authenticated endpoint, provide the credentials or token your test environment expects. See the API testing guide for request patterns, authentication, error responses, and combining API calls with UI tests.

Example: a component check

Component testing setup depends on the framework and bundler in your project, so use the Cypress App’s component setup flow and its generated mount support file. Once configured, a component test mounts the component directly rather than navigating through the full application:

// Example shape; import and mount support depend on your framework
import { mount } from 'cypress/react'
import SaveButton from '../../src/SaveButton'

describe('SaveButton', () => {
  it('calls its handler when clicked', () => {
    const onSave = cy.stub().as('onSave')
    mount(<SaveButton onSave={onSave} />)
    cy.contains('button', 'Save').click()
    cy.get('@onSave').should('have.been.calledOnce')
  })
})

Use the framework-specific component testing setup guide for the correct mount adapter and imports. The example’s import is not interchangeable across React, Vue, Angular, and other setups.

3. Decide when to use real responses and when to stub

Use a real backend response when a test must establish that the client and server work together: the request reaches the real service, the response has the expected shape, and the UI consumes it. These tests need a dependable environment and often seeded data, but they cover the integration contract.

Use cy.intercept() when you need a predictable response, want to force an edge case, or need to observe and wait for a request without depending on an external service. A stub proves how the UI handles the response you supplied; it does not prove the real service returns that response.

describe('orders', () => {
  it('shows an empty state for an account with no orders', () => {
    cy.intercept('GET', '/api/orders', {
      statusCode: 200,
      body: [],
    }).as('getOrders')

    cy.visit('/orders')
    cy.wait('@getOrders')
    cy.contains('No orders yet').should('be.visible')
  })

  it('shows a service error', () => {
    cy.intercept('GET', '/api/orders', {
      statusCode: 503,
      body: { message: 'Service unavailable' },
    }).as('getOrders')

    cy.visit('/orders')
    cy.wait('@getOrders')
    cy.contains('Orders could not be loaded').should('be.visible')
  })
})

Intercepts can match a URL, method, or route matcher; inspect requests, assert their details, change a request, or return a static or dynamic response. Name routes with aliases and wait on the specific request instead of sleeping for an arbitrary interval. Cypress clears intercepts before each test. The network requests guide covers these patterns and tradeoffs.

  • Real service: critical integration and contract paths; requires reachable services and controlled data.
  • Stubbed service: deterministic loading, empty, validation, error, and unusual response cases; cannot independently validate the backend contract.
  • Mixed approach: use a real request for the main integration path and stubs for cases that are costly or difficult to produce reliably in a real environment.

4. Add accessibility checks with clear limits

Accessibility testing can combine semantic assertions in ordinary Cypress tests, automated scans from a plugin, and Cypress’s accessibility offering. Check critical interactions such as whether controls have accessible names and whether validation errors are associated with their fields. Automated scans can find some known violations, but they do not prove that a page works well for people with disabilities. Include keyboard and assistive-technology evaluation in your broader accessibility process.

it('gives the email field an accessible label', () => {
  cy.visit('/signup')
  cy.get('label[for=email]').should('contain', 'Email')
  cy.get('#email').should('be.visible')
})

See Cypress’s accessibility testing guide for supported approaches and their limitations.

5. Make CI runs deterministic

The test runner must not start before the application server is ready. Starting a server in the background and immediately invoking Cypress creates a race: Cypress may visit the app before it is listening. Use a readiness check that polls the server, then start the tests. Do not substitute a fixed sleep; machine load and startup time vary.

# Example using the wait-on package
npm install --save-dev wait-on

# Terminal or CI script
npm run start:test &
npx wait-on http://127.0.0.1:3000
npx cypress run --browser chrome

Adapt the start command and URL to your app, and ensure the background process is cleaned up by your CI job. For a real pipeline, use the CI provider’s process management or Cypress’s supported action integration. Cypress documents common providers and the GitHub Action’s start and wait-on options in its CI overview and official GitHub Action.

A compact GitHub Actions shape is below. Pin action versions according to your repository policy and consult the action documentation for current inputs:

name: Cypress
on: [push, pull_request]
jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - uses: cypress-io/github-action@v6
        with:
          start: npm run start:test
          wait-on: 'http://127.0.0.1:3000'
          browser: chrome

Keep the example’s Node.js version aligned with the current Cypress requirements and your project runtime. Make CI match local dependency installation with npm ci and a committed lockfile. Cypress’s installation guidance suggests at least 2 CPUs and 4 GB RAM for CI, and 8 GB or more for longer runs or video recording; treat this as vendor guidance, then observe your workload and runner resource limits.

CI checklist

  • Install the project from its lockfile and ensure Cypress’s browser binary is available.
  • Start the application with a predictable test configuration and data source.
  • Wait for a successful readiness response before launching Cypress.
  • Use stable test data, selectors, and independent tests; avoid ordering dependencies.
  • Capture enough output to diagnose failures, and distinguish product defects from infrastructure failures.
  • Keep secrets in the CI secret store, not in committed test code or logs.

6. Select browser coverage deliberately

Cypress supports Chrome-family browsers and Firefox, and documents WebKit support as experimental. Browsers used in a test environment must be installed there. Cypress launches its own browser instance rather than attaching to your everyday open session. Electron is deprecated as a test browser, so explicitly select a supported installed browser such as Chrome in CI.

npx cypress run --browser chrome
npx cypress run --browser firefox

Choose coverage based on the browsers your users rely on, the risk of browser-specific behavior, test duration, and CI capacity. A common strategy is to run the complete suite in one primary browser for fast feedback and selected critical paths in additional browsers on a schedule or before release. WebKit is experimental; do not treat it as equivalent to a stable, generally supported Safari test environment. Check the live cross-browser guide and browser launch reference before changing your matrix.

7. Reliability, performance, and cost tradeoffs

Reliability: tests become more predictable when each one controls its own data, waits for observable conditions, and uses selectors intended for tests. Wait on a route alias or a visible state rather than guessing how long an operation takes. Keep external systems out of tests that do not need to validate them. When a test does need a real service, make its availability and data lifecycle part of the test environment design.

Performance: component and direct API checks usually have a narrower scope than full browser journeys. E2E tests incur browser and app setup and can traverse more layers. Stubbing reduces dependence on network variability but does not replace real integration coverage. For practical speed, keep E2E journeys focused on high-value paths, move isolated behavior to component or API tests, and run a measured browser matrix instead of multiplying every spec across every browser by default.

Cost: Cypress App is locally installed software described by Cypress as free. CI cost comes from runner time, browser installation, and infrastructure for the app and backend. Cypress Cloud is an optional paid service for recorded runs, test results, and analytics; check Cypress’s current plan information for pricing and feature availability. The dossier does not establish current prices, so this guide does not quote them. Cypress’s CI documentation discusses duration and infrastructure tradeoffs.

8. Troubleshoot common Cypress failures

Symptom Likely cause What to do
Cannot visit the app or connection is refused The app is stopped, the URL or port is wrong, or the CI test starts before the server is ready Confirm the app responds at the configured baseUrl; add a readiness wait before Cypress runs.
Browser not found or launch fails in CI The selected browser is not installed or is not detected on that runner Install an appropriate supported browser in the environment and pass its name with --browser. Check the browser launch documentation.
A request alias times out The route matcher does not match the actual method or URL, the request never occurs, or a cached response avoids a network request Inspect the Command Log and actual request URL; tighten or correct the matcher and ensure the app triggers the request. Cypress notes that a response served from browser cache does not pass through the network interception layer.
Test passes locally but fails intermittently in CI Timing assumptions, shared state, resource pressure, or reliance on an external service Replace arbitrary waits with retryable assertions or route waits; isolate test data; inspect runner capacity and service health.
Stub has no effect The method or URL does not match, intercept was registered after the request, or response shape differs from what the app expects Register the intercept before visiting or triggering the request, match the correct route, and provide the expected status, headers, and body.
Component test mount import fails The component testing framework adapter or generated support configuration is missing or differs from the example Run the Cypress component setup for your framework and use its generated mount helper and imports.
Install fails with unsupported runtime or OS Node.js, operating system, package manager, or system libraries do not meet current requirements Compare the environment with the live install requirements and update the runtime or dependencies accordingly.

For browser and installation errors, consult the live installation requirements and browser troubleshooting guidance. For intercept behavior, consult the cy.intercept() API reference.

Or skip the browser setup

Cypress is for testing application behavior. If a workflow also needs a clean screenshot of a webpage—for documentation, review, or an agent workflow—ScreenshotNeo is the screenshot API and MCP server from Yorker Media. It takes a screenshot or PDF with one GET request. See the ScreenshotNeo API documentation.

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);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, and cache hits are not billed; responses identify the page verdict and billing status in headers. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card.

FAQ

How do I run a Cypress test?

Run npx cypress open for the interactive App or npx cypress run for headless execution. Ensure the application is running when the spec visits it.

Can Cypress test APIs without opening a browser?

Yes. Use cy.request() in an E2E spec to call an endpoint directly. Cypress classifies API testing within its E2E testing type.

Does a passing stubbed test prove the backend works?

No. It shows that the application responds as expected to the response your test supplied. Include real-response checks for critical client-server integration paths.

Does an automated accessibility scan prove a page is accessible?

No. Automated tools identify some detectable rule violations. They cannot establish the complete experience for people with disabilities.

Is Cypress Cloud required?

No. Cypress App runs locally and in CI without Cloud. Cloud is an optional paid service for recorded runs and analytics; check current Cypress materials for its plans and features.