Cypress Tutorial for Beginners

Updated June 2026
This tutorial walks you through installing Cypress, writing your first end-to-end test, using selectors to find elements, adding assertions to verify behavior, and organizing your test suite as it grows. By the end, you will have a working Cypress setup and the confidence to write tests for real web applications.

Cypress is a JavaScript testing framework that runs directly in the browser, giving you instant visual feedback as your tests execute. Unlike older testing tools that require complex driver configurations and multiple dependencies, Cypress installs with a single command and works out of the box. This guide assumes you have basic familiarity with JavaScript and HTML, but no prior testing experience is required.

Step 1: Install Node.js and Create a Project

Cypress requires Node.js version 18 or later. If you do not already have Node.js installed, download the LTS release from the official Node.js website and follow the installer for your operating system. You can verify your installation by running node --version in a terminal.

Once Node.js is installed, create a new project directory and initialize it with npm:

mkdir my-cypress-project
cd my-cypress-project
npm init -y

The npm init -y command creates a package.json file with default settings. If you are adding Cypress to an existing project, skip this step and move directly to the installation.

Step 2: Install Cypress

Install Cypress as a development dependency using npm, yarn, or pnpm:

npm install cypress --save-dev

This downloads the Cypress application binary along with the Node.js module. The binary is cached in your home directory at ~/.cache/Cypress, so subsequent installs in other projects are faster. The download is approximately 250 MB and includes a bundled Electron browser for running tests.

After installation completes, verify that Cypress is available by running:

npx cypress --version

You should see the Cypress version number printed to the terminal. If you encounter permission errors on Linux, make sure your user has write access to the npm cache directory.

Step 3: Open Cypress for the First Time

Launch the Cypress Launchpad with:

npx cypress open

The first time you run this command, Cypress creates its default directory structure and configuration file. The Launchpad asks you to choose between E2E Testing and Component Testing. Select E2E Testing to get started with full-page tests that simulate real user interactions.

Cypress then asks which browser you want to use. It detects all compatible browsers installed on your system, including Chrome, Edge, Firefox, and the bundled Electron browser. Choose Chrome or Electron for the fastest experience. You can switch browsers at any time from the Test Runner's browser selector dropdown.

After selecting a browser, Cypress creates the following directory structure in your project:

cypress/
  e2e/           # Your test files go here
  fixtures/      # Test data files (JSON, etc.)
  support/
    commands.js  # Custom commands
    e2e.js       # Global configuration for e2e tests

The cypress.config.js file appears in your project root. This is where you configure settings like the base URL, viewport dimensions, and timeout values.

Step 4: Write Your First Test

Create a new file called cypress/e2e/first-test.cy.js. The .cy.js extension tells Cypress this is a test specification file. Open the file in your editor and add the following:

describe('My First Cypress Test', () => {
  it('visits the Cypress documentation site', () => {
    cy.visit('https://docs.cypress.io');
    cy.title().should('include', 'Cypress');
  });
});

This test does two things: it navigates to the Cypress documentation website, then verifies that the page title contains the word "Cypress." The describe block groups related tests together, and each it block defines a single test case. This syntax comes from Mocha, the test runner that Cypress uses internally.

Save the file and switch to the Cypress Test Runner window. Your new test file appears in the spec list. Click it to run the test. You will see a real browser open, navigate to the URL, and the Command Log on the left will show each step as it executes.

For testing your own application, update cypress.config.js to set a base URL:

const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
  },
});

With a base URL configured, cy.visit('/') navigates to your application's homepage without typing the full URL in every test.

Step 5: Use Selectors to Find Elements

The cy.get() command is how you select elements on the page. It accepts any CSS selector, but some strategies produce more resilient tests than others.

The recommended approach is to use data-testid attributes on your HTML elements. These attributes are invisible to users but provide stable anchors for tests that do not break when CSS classes or element hierarchy changes:

<button data-testid="submit-btn">Submit</button>

// In your test:
cy.get('[data-testid="submit-btn"]').click();

When data-testid attributes are not available, you can use standard CSS selectors. Prefer selectors that are specific enough to target one element but general enough to survive minor HTML changes:

// By ID (unique per page)
cy.get('#login-form');

// By class with element type
cy.get('button.primary');

// By attribute
cy.get('input[name="email"]');

// By text content
cy.contains('Sign In');

Avoid selectors that depend on CSS styling classes like .btn-primary-lg or deep DOM paths like div > div > ul > li:nth-child(3). These break whenever the UI is redesigned or the component structure changes. The cy.contains() command is especially useful for buttons and links where the visible text is the most stable identifier.

Step 6: Add Assertions to Verify Behavior

Assertions verify that your application behaves correctly. Cypress uses the Chai assertion library, accessed primarily through the .should() command that chains onto any Cypress query:

// Element visibility
cy.get('.modal').should('be.visible');
cy.get('.loading-spinner').should('not.exist');

// Text content
cy.get('h1').should('have.text', 'Welcome');
cy.get('.message').should('contain', 'saved');

// Form values
cy.get('input[name="email"]').should('have.value', 'user@test.com');

// Element state
cy.get('button').should('be.disabled');
cy.get('.menu-item').should('have.class', 'active');

// URL and title
cy.url().should('include', '/dashboard');
cy.title().should('equal', 'Dashboard - My App');

Assertions in Cypress automatically retry until they pass or the timeout expires. If you assert that a list should have 5 items but the API response has not arrived yet, Cypress waits and re-checks until the items appear. This retry behavior is built into every assertion, so you rarely need to add explicit waits.

You can chain multiple assertions on the same element:

cy.get('.notification')
  .should('be.visible')
  .and('contain', 'Changes saved')
  .and('have.class', 'success');

For more complex assertions, use .then() to access the element and write custom logic:

cy.get('.item-count').then(($el) => {
  const count = parseInt($el.text(), 10);
  expect(count).to.be.greaterThan(0);
});

Step 7: Organize Your Test Suite

As your test suite grows, organization becomes important for maintainability and readability. Structure your test files by feature or page rather than by test type:

cypress/e2e/
  auth/
    login.cy.js
    signup.cy.js
    password-reset.cy.js
  dashboard/
    overview.cy.js
    settings.cy.js
  products/
    catalog.cy.js
    checkout.cy.js

Use beforeEach hooks for shared setup that runs before every test in a describe block. Common uses include visiting a page, logging in, or seeding test data:

describe('Dashboard', () => {
  beforeEach(() => {
    cy.visit('/login');
    cy.get('[data-testid="email"]').type('test@example.com');
    cy.get('[data-testid="password"]').type('password123');
    cy.get('form').submit();
  });

  it('shows the overview panel', () => {
    cy.get('.overview-panel').should('be.visible');
  });

  it('displays recent activity', () => {
    cy.get('.activity-feed').should('exist');
  });
});

For login flows that repeat across many test files, create a custom command in cypress/support/commands.js:

Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('[data-testid="email"]').type(email);
  cy.get('[data-testid="password"]').type(password);
  cy.get('form').submit();
  cy.url().should('include', '/dashboard');
});

// Use in tests:
beforeEach(() => {
  cy.login('test@example.com', 'password123');
});

Custom commands keep your tests readable and reduce duplication. When the login flow changes, you update the command in one place rather than modifying dozens of test files.

Key Takeaway

Cypress lowers the barrier to front-end testing by handling browser management, waiting, and retries automatically. Start with simple tests that visit pages and check visible text, then gradually add interaction tests, network stubs, and custom commands as your confidence grows.