Resource library

QA How-To

Getting Started with Cypress

Learn Cypress end-to-end testing from installation through reliable selectors, network control, debugging, fixtures, and a practical CI workflow.

1,821 words | Article schema | FAQ schema | Breadcrumb schema

Overview

Cypress gives frontend and QA engineers an unusually transparent way to learn browser testing. Tests run with a live command log, automatic screenshots, time-travel snapshots, and direct visibility into application network traffic. Its commands retry while the page changes, so a well-written test can follow dynamic interfaces without scattered sleeps. That fast feedback makes Cypress especially approachable for teams that already use JavaScript or TypeScript.

The convenience has rules. Cypress commands are queued, subjects flow through chains, and browser state must remain controlled. Treating commands like ordinary synchronous JavaScript leads to confusing tests. This guide builds a complete first suite while explaining that execution model, then covers selectors, assertions, network interception, reusable setup, debugging, and CI. The examples use an online store, but the design applies to most React, Vue, Angular, or server-rendered applications.

Install Cypress and Open the Test Runner

Start inside the web application's repository so Cypress can share its package scripts and configuration. Run `npm install --save-dev cypress`, then `npx cypress open`. Choose end-to-end testing, accept the proposed support files, select a browser, and create an empty spec. Cypress writes `cypress.config.ts` plus folders for tests, fixtures, and support code. The open command launches interactive mode, where each saved change reruns the selected spec.

Add package scripts for `cypress open` and `cypress run`. Interactive mode is for authoring and investigation. Run mode is headless by default and is what CI normally executes. Confirm that an empty smoke spec loads before adding application code. If the app is local, start it separately and verify its URL in the same environment where Cypress runs. This separates runner installation failures from application failures.

  • Install Cypress as a project development dependency.
  • Use open mode while writing and run mode in automation.
  • Commit configuration, specs, support code, fixtures, and the package lockfile.
  • Ignore generated videos, screenshots, and downloads unless the team intentionally archives them.

Configure a Stable Base URL

Set `e2e: { baseUrl: 'http://localhost:3000' }` inside `defineConfig` in `cypress.config.ts`. Tests can then use `cy.visit('/products')` instead of repeating a hostname. Override environment-specific values through configuration or CI environment variables rather than branching inside tests. A single spec should describe behavior the same way in development, staging, and an ephemeral preview environment. Log the selected target at run startup so reports cannot be mistaken for another deployment.

Cypress resets cookies, local storage, and the page between tests to promote isolation. Preserve that default. A test that passes only because an earlier test logged in or created a cart is order dependent and will fail when filtered or parallelized. Use `beforeEach` for lightweight common navigation, API setup for required records, and `cy.session()` for validated login sessions when repeated UI login becomes expensive.

Write the First End-to-End Scenario

Create `cypress/e2e/cart.cy.ts`: `describe('shopping cart', () => { it('adds an available item', () => { cy.visit('/products'); cy.contains('[data-cy=product-card]', 'Trail Bottle').within(() => { cy.get('[data-cy=add-to-cart]').click(); }); cy.get('[data-cy=cart-count]').should('have.text', '1'); cy.contains('Added Trail Bottle').should('be.visible'); }); });`. `describe` groups behavior, and `it` names a user outcome. The assertions prove both the cart count and feedback message. They also give two diagnostic signals when only one part of the interface updates.

Cypress puts commands such as `visit`, `get`, `click`, and `should` into an internal queue. They do not return page values in the way normal synchronous functions do. The subject from one command flows to the next, and assertions retry until they pass or time out. Keep chains readable and avoid storing a command result in a regular variable. When access to a yielded value is necessary, use `.then(value => { ... })`, an alias, or continue chaining from the subject.

Design Selectors for People and Tests

Selectors should identify purpose, not styling. Classes such as `.mt-4.rounded.primary` change during redesigns, and text changes during copy updates or localization. For critical controls, add attributes such as `data-cy=checkout` that are explicitly stable for automation. User-facing text is still appropriate when the wording itself matters, and semantic queries from Testing Library can be valuable when accessibility is a testing goal.

Scope repeated content with `.within()` or start from a card containing a unique name. A selector should not depend on the item's current list index unless sorting is the requirement. If `cy.get()` finds multiple subjects, Cypress may apply an action in an unintended way or reject it. Refine the selector instead of appending `.first()` automatically. The best selector strategy is documented with developers and reviewed like an API contract.

  • Use stable data attributes for controls whose visible identity is ambiguous.
  • Use text when copy is part of the expected behavior.
  • Scope searches to a product card, dialog, table row, or form.
  • Avoid CSS presentation classes and DOM-depth selectors.
  • Make selector uniqueness intentional rather than relying on list order.

Let Queries and Assertions Retry

Cypress automatically retries query chains and their assertions. `cy.get('[data-cy=save-status]').should('contain', 'Saved')` keeps querying until the expected text appears or the timeout expires. An action such as `click` waits for actionability, but the action itself is not repeatedly performed. This distinction prevents accidental double submissions while still accommodating normal rendering delays. Read the command log to see which query retried and which action ran once.

Never replace uncertainty with `cy.wait(5000)`. A fixed delay always spends five seconds and may still be too short. Wait on a user-visible state or alias a relevant request. For an animation, assert the final component state. For a background job, poll a supported API or wait for the status request. Increase a command-specific timeout only when the operation has a legitimate documented duration, and include enough context in the assertion to reveal what was expected.

Control and Observe Network Requests

Use `cy.intercept()` to spy on or stub HTTP traffic. Register it before the action: `cy.intercept('POST', '/api/orders').as('createOrder'); cy.get('[data-cy=place-order]').click(); cy.wait('@createOrder').its('response.statusCode').should('eq', 201); cy.contains('Order confirmed').should('be.visible');`. The request assertion proves the expected integration occurred, while the UI assertion confirms the user received the successful result. Inspect the intercepted request body too when submitted values are part of the contract, such as price, currency, or product identifiers.

Stubbing makes difficult states deterministic. `cy.intercept('GET', '/api/recommendations*', { statusCode: 500, body: { message: 'Unavailable' } }).as('failedRecommendations')` lets you verify error handling without breaking a shared service. Do not stub every call in an end-to-end suite, or you only test the frontend against your assumptions. Keep a clear boundary between integrated journey tests and UI-focused tests with controlled responses, and name specs or tags so reports show that distinction.

Manage Test Data, Login, and Custom Commands

Fixtures are static files under `cypress/fixtures`, useful for representative response bodies and read-only data. They are not a substitute for unique records in a mutable environment. Create required users or orders through `cy.request()` against a supported test endpoint, capture returned IDs, and clean them up if the environment does not expire them. API setup is faster and less brittle than navigating multiple screens before the actual scenario begins.

Use `cy.session()` to cache and restore browser session data while validating that the session is still usable. Keep credentials in Cypress environment values or a secret manager, not fixture files. Custom commands are appropriate for stable domain actions such as `cy.loginAs('manager')`, but avoid turning every `get` and `click` into a wrapper. A command should reduce meaningful repetition and have a typed signature. Excessive abstraction hides Cypress's command chain and makes failure locations harder to understand.

Debug Through the Command Log

In open mode, click any command in the runner to inspect the DOM snapshot before and after it. Read the selector, yielded subject, console properties, request log, and assertion message. Add `.debug()` to pause with the current subject in developer tools, or `cy.pause()` while stepping through a chain. Browser console errors and failed network calls often explain a missing element better than modifying the selector.

Headless failures should preserve screenshots and, when configured, video. CI output includes the command and assertion that timed out. Reproduce with the same viewport, browser, configuration, and test data. Classify whether the failure is a product defect, an unstable test, unavailable environment, or data collision. Cypress retries can reveal intermittent behavior and protect against rare infrastructure issues, but a test that routinely passes on retry is a defect to investigate, not a successful test.

Build a Fast and Trustworthy CI Suite

A CI job should install from the lockfile, build or start the application, wait for its health endpoint, and run Cypress in run mode. Use a purpose-built action or a process tool that shuts the server down even when tests fail. Upload screenshots, videos, and machine-readable results as artifacts. Split the suite across workers only after every spec owns its data and state, otherwise parallelism turns hidden dependencies into random failures.

Run a concise smoke group on pull requests and broader regression against production-like builds. Component tests can exercise frontend states more cheaply, while API and unit tests should cover most rules. Reserve end-to-end Cypress tests for critical workflows and browser integration. Review slow specs, repeated backend setup, and flaky failure clusters. A suite that returns a trustworthy answer in ten minutes provides more release value than a huge suite engineers routinely rerun or ignore.

Frequently Asked Questions

Is Cypress easy for beginners?

Cypress has a friendly interactive runner and bundles many testing features, so the first useful test is quick to create. Beginners must learn its queued command model, because Cypress commands do not behave like ordinary synchronous return values.

Can Cypress test APIs without a browser UI?

Yes. `cy.request()` can call APIs for assertions or test setup, and `cy.intercept()` can observe or control requests made by the application. For large standalone API suites, a dedicated API client may offer a cleaner execution model.

Why should I avoid cy.wait with a number?

A numeric wait spends the full duration regardless of readiness and still fails when the environment is slower. Assert the visible result or wait on an intercepted request alias that represents the operation.

What selector should I use in Cypress?

Prefer stable testing attributes for ambiguous controls and semantic queries when accessible behavior matters. Avoid selectors based on CSS styling, nested structure, or current list position because routine UI refactoring breaks them.

Does Cypress support multiple browser tabs?

Cypress centers a test on one browser tab. Teams commonly verify a link's destination attribute or remove the target and continue in the same tab. If multi-tab interaction is a core requirement, evaluate whether another browser automation tool fits that scenario better.

When should I use cy.session?

Use it when repeated authentication is expensive and the restored session can be validated safely. The test must still own its business data, and secrets used to create the session should remain outside source control.

Related QAJobFit Guides