PRACTICAL GUIDE / cypress tutorial for beginners
Cypress Tutorial for Beginners: Your First E2E Test
Cypress tutorial for beginners covering setup, your first E2E test, selectors, assertions, fixtures, debugging, CI, and stable automation habits.
In this guide9 sections
- Define the first reliable journey
- Add Cypress to the repository
- Make application state deliberate
- Write the search-to-cart test
- Choose selectors that have an owner
- Control network dependencies without hiding the product
- Diagnose failures from evidence
- Put a small suite in CI
- Grow the suite only after it earns trust
- Review a spec as a release signal
What you will learn
- Define the first reliable journey
- Add Cypress to the repository
- Make application state deliberate
- Write the search-to-cart test
A checkout test that passes locally and fails twice a week in CI is worse than having no checkout test. The team starts rerunning the job, genuine regressions become background noise, and nobody can explain whether the failure came from the product, the data, or the test. A useful first Cypress project should solve that trust problem before it grows a large suite.
This guide builds a small test around a product search and cart flow. The example uses a controlled starting state, selectors owned by the application, a network boundary, and assertions on durable outcomes. Those choices matter more than learning a long list of Cypress commands.
Define the first reliable journey
Suppose the application has a catalog at /products. A shopper searches for "trail mug," opens the matching item, and adds it to the cart. The critical behavior is not that several buttons can be clicked. It is that the search returns the intended product and the cart records the selected SKU.
Write the test contract before the code:
- The test starts with an empty cart and a known catalog record.
- Search readiness is tied to the catalog response, not a fixed delay.
- Elements are addressed by accessible roles or stable
data-cyhooks. - The final assertion proves both visible feedback and persisted cart state.
This scope is intentionally narrow. Account creation, payment, and recommendations belong in other tests. A short test has fewer unrelated failure modes and tells the team what broke.
Add Cypress to the repository
Install Cypress beside the web application so its scripts and environment configuration stay versioned with the product.
npm install --save-dev cypress @testing-library/cypress
npx cypress openAdd explicit commands to package.json instead of relying on team members to remember flags.
{
"scripts": {
"test:e2e:open": "cypress open",
"test:e2e": "cypress run --browser chrome"
}
}Keep the base URL and conservative retry behavior in configuration. Cypress automatically retries queries and assertions, but test retries rerun a failed test from the beginning. They can reduce transient infrastructure noise, yet they must not become a substitute for diagnosis.
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: process.env.APP_URL || 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.js',
supportFile: 'cypress/support/e2e.js'
},
retries: {
runMode: 1,
openMode: 0
},
video: false,
screenshotOnRunFailure: true
})A checked-in baseUrl default makes local execution easy, while APP_URL lets CI target its deployed test environment. Do not put passwords or tokens in this file. Inject secrets through the CI secret store.
Make application state deliberate
UI setup is often the first source of suite slowness. If every cart test signs up, signs in, clears the cart through menus, and navigates through promotions, setup failures obscure the cart behavior. Use an API or a dedicated test task for preconditions the test is not evaluating.
// cypress/support/commands.js
Cypress.Commands.add('resetCart', () => {
cy.request('POST', '/test-support/cart/reset', {
customerId: 'e2e-shopper'
}).its('status').should('eq', 204)
})// cypress/support/e2e.js
import './commands'
import '@testing-library/cypress/add-commands'The endpoint should exist only in a protected test environment, authenticate the request, and return after the state is committed. If the endpoint queues an asynchronous reset and returns early, the browser can still see the old cart. In that case, return a job result that proves completion or poll a read endpoint with a bounded strategy.
Use unique records when tests can run concurrently. A constant customer ID is safe only if this spec is serialized. Parallel workers need a per-test ID derived from a CI worker value or generated by the backend.
Write the search-to-cart test
The spec observes the catalog request because its completion is the product's meaningful readiness signal. It does not stub the response, so this test covers the integration between UI and catalog API.
// cypress/e2e/cart/search-and-add.cy.js
describe('catalog cart flow', () => {
beforeEach(() => {
cy.resetCart()
})
it('adds the searched SKU to an empty cart', () => {
cy.intercept('GET', '**/api/products?query=*').as('catalogSearch')
cy.visit('/products')
cy.findByRole('searchbox', { name: /search products/i })
.type('trail mug')
cy.wait('@catalogSearch')
.its('response.statusCode')
.should('eq', 200)
cy.contains('[data-cy="product-card"]', 'Trail Mug')
.within(() => {
cy.findByRole('button', { name: /add to cart/i }).click()
})
cy.findByRole('status').should('contain.text', 'Added to cart')
cy.get('[data-cy="cart-count"]').should('have.text', '1')
cy.request('/api/cart/e2e-shopper')
.its('body.items')
.should('deep.include', { sku: 'MUG-TRAIL', quantity: 1 })
})
})The findByRole commands come from Cypress Testing Library, installed and imported during setup above. If the project does not choose that dependency, use application-owned selectors such as cy.get('[data-cy="product-search"]') instead.
The final API assertion is valuable because a toast can appear before persistence fails. It also introduces coupling to the cart contract, so keep it focused on the SKU and quantity that matter. Avoid snapshotting the entire response.
Choose selectors that have an owner
Good selectors express a user contract or a testing contract. A labeled input and a button role are usually durable because accessibility depends on them. A data-cy attribute is appropriate when repeated cards or canvas-driven controls lack an unambiguous accessible locator.
Avoid selectors such as .grid > div:nth-child(2) .btn-primary. Styling refactors can break them without changing behavior. Avoid broad text matching when translations or duplicate labels are expected. Ask developers to add a test hook rather than forcing the test to reverse-engineer layout.
Cypress commands are queued, so assigning their results to ordinary variables often surprises new users. Continue the chain or use .then() when a later step needs a yielded value.
cy.get('[data-cy="order-number"]')
.invoke('text')
.then((orderNumber) => {
expect(orderNumber.trim()).to.match(/^ORD-/)
})Use .then() for transformation, not as a manual wait. Cypress retries assertions attached to queries, while arbitrary code inside .then() is not rerun in the same way.
Control network dependencies without hiding the product
cy.intercept() can observe real requests or replace responses. Observation is appropriate for a small end-to-end journey. Stubbing is useful for rare errors, slow states, or deterministic UI checks.
cy.intercept('GET', '**/api/products?query=trail%20mug', {
statusCode: 503,
body: { error: 'catalog unavailable' }
}).as('catalogFailure')
cy.visit('/products')
cy.get('[data-cy="product-search"]').type('trail mug')
cy.wait('@catalogFailure')
cy.findByRole('alert').should('contain.text', 'Try again')This test proves error rendering, not that the live catalog produces a 503 correctly. Name and place stubbed specs so reviewers understand the boundary. A suite made entirely of stubs can be fast and green while the deployed services are incompatible.
Never replace an arbitrary sleep with cy.wait('@alias') unless that request is truly the readiness condition. Some pages issue background analytics forever, while the content becomes ready earlier. Wait for the smallest observable signal that allows the next user action.
Diagnose failures from evidence
Start with Cypress's command log in open mode. Inspect the DOM snapshot at the failing command, the request and response attached to an alias, the browser console, and the failure screenshot. Classify the result before changing the test:
- A locator finds nothing after a UI refactor: selector maintenance.
- The request returns 500: product or environment failure.
- The expected record belongs to another worker: data isolation failure.
- The button is covered by a loading overlay: synchronization or product usability issue.
- The test passes only when rerun: unresolved flake, not a successful build.
Add targeted logging for IDs and response status, but do not dump access tokens or entire customer records into CI output. If a failure cannot be reconstructed from saved evidence, improve the evidence before increasing retries.
Put a small suite in CI
CI must start the application, wait until its health endpoint is ready, and then run Cypress. The following job assumes the app has an npm run start:test script and a stable /health route.
name: e2e
on: [pull_request]
jobs:
cypress-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: npm
- run: npm ci
- run: npm run start:test &
- run: npx wait-on http://127.0.0.1:3000/health
- run: npm run test:e2e -- --spec 'cypress/e2e/cart/**/*.cy.js'
env:
APP_URL: http://127.0.0.1:3000
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-failures
path: cypress/screenshotsPin dependencies with the lockfile and install with npm ci. Run a small, release-relevant group on pull requests, then broader coverage on a schedule if runtime warrants it. Before promoting a test to the blocking group, run it repeatedly under CI-like conditions and remove every dependency on execution order.
Grow the suite only after it earns trust
The next tests should cover a distinct risk: an empty search result, an unavailable catalog, removing a cart line, or preventing duplicate submission. Keep each test independently runnable and make data ownership visible. Extract a command only after repetition is understood; early abstraction can hide what state a test requires.
Review a spec as a release signal
Before adding a spec to the pull-request gate, ask another engineer to diagnose three seeded failures: an API error, a changed selector, and missing setup data. They should be able to identify the likely owner from the command log and artifacts without reading every helper. If all three failures end as "element not found," the test is hiding its boundaries.
Track why each blocking test exists. Link it to a critical journey, production regression, or explicit release risk in the test description or suite documentation. This makes deletion possible when the feature changes. Tests with no stated risk tend to accumulate weak assertions because nobody knows what they are supposed to protect.
Run specs in a randomized order occasionally and run the same spec twice against a clean environment. The first check exposes shared browser or backend state. The second exposes setup that creates fixed records and cleanup that assumes only one execution. These exercises are more useful than repeating a stable happy path many times.
Treat accessibility improvements as testability improvements. A correctly labeled search input and a uniquely named add button give assistive technology and Cypress the same durable contract. When a role query becomes ambiguous, inspect whether the product itself presents two controls with indistinguishable names before narrowing the test.
A healthy first milestone is not a large test count. It is a documented command that another engineer can run, a failure artifact they can interpret, and a small set of checks the team will not casually rerun. Once those checks stay reliable through real product changes, extend the suite with the same state, selector, boundary, and evidence discipline.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01
- 02Cypress best practices
Cypress
Canonical recommendations for selectors, state, test isolation, and assertions.
- 03
FAQ / QUICK ANSWERS
Questions testers ask
Is Cypress good for beginners?
Yes. Cypress is beginner friendly because installation is simple, the test runner is visual, and commands automatically retry. Beginners still need to learn selectors, assertions, test data control, and how the browser event loop affects testing, but the first useful test is usually faster than with older browser tools.
What should I learn before Cypress?
You should know basic JavaScript, HTML selectors, the browser developer tools, and the difference between user behavior and implementation detail. You do not need advanced frontend knowledge to start, but you will write better Cypress tests if you understand how forms, network calls, and page state work.
Can Cypress test APIs?
Cypress can make HTTP requests with cy.request and can stub or inspect network calls with cy.intercept. It is not a full API testing platform by itself, but it is very useful for setting up data, checking backend responses that support an E2E flow, and reducing dependence on the UI for repetitive setup.
Is Cypress better than Selenium?
Cypress is usually easier for modern web app E2E testing, especially for JavaScript teams. Selenium is broader because it supports more languages and remote browser grids. The better choice depends on browser coverage, team skills, application architecture, and whether you need cross language or large scale grid execution.
How many Cypress tests should a beginner write first?
Start with three to five high value flows: sign in, one critical create action, one validation path, one navigation path, and one regression for a past bug. That gives you enough practice with selectors, assertions, setup, and debugging without creating a large suite before you understand maintenance costs.
RELATED GUIDES
Continue the learning route
GUIDE 01
Cypress Best Practices
Cypress best practices for stable E2E tests: selectors, waits, isolation, Page Objects, network stubs, CI tips, and common mistakes to avoid in real projects.
GUIDE 02
Cypress vs Playwright 2026: Which Tool Should You Choose?
Cypress vs Playwright 2026 comparison for QA teams choosing E2E tools, covering speed, browsers, debugging, API testing, CI, and overall team fit.
GUIDE 03
Playwright Tutorial: End-to-End Testing from Scratch
Playwright tutorial for beginners: install, write your first test, TypeScript setup, codegen, fixtures, API testing, debugging, and CI tips in one guide.
GUIDE 04
Selenium vs Playwright vs Cypress in 2026
Compare Selenium vs Playwright vs Cypress in 2026 for speed, browsers, CI, flakiness, ecosystem, and which web automation framework beginners should choose.