Getting Started with Cypress: Your First E2E Test in 15 Minutes

Getting Started with Cypress: Your First E2E Test in 15 Minutes

Cypress is one of the most developer-friendly testing frameworks available. It runs in the browser, shows you exactly what's happening in real time, and has an interactive UI that makes debugging failures fast. If you've never written a test before — or you're coming from another framework — this guide gets you to a passing test in 15 minutes.

What Makes Cypress Different

Most E2E testing tools work from outside the browser, communicating via WebDriver. Cypress runs inside the browser alongside your application. This architecture means:

  • Faster execution — no network roundtrips between test runner and browser
  • Real-time reloads — tests re-run when files change
  • Time travel — click any command in the UI to see what the DOM looked like at that moment
  • Native access — manipulate the DOM, stub network requests, and control browser state directly

The tradeoff: Cypress currently supports Chromium-based browsers and Firefox, but not Safari.

Step 1: Install Cypress

In an existing project:

npm install --save-dev cypress

Open Cypress for the first time:

npx cypress open

The Cypress Launchpad opens. Choose E2E Testing and follow the setup wizard. It creates cypress.config.ts and a cypress/ folder with example specs.

Step 2: Configure Your Base URL

Open cypress.config.ts and add your app's URL:

import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    setupNodeEvents(on, config) {},
  },
});

With baseUrl set, you can use relative paths in cy.visit().

Step 3: Write Your First Test

Create cypress/e2e/homepage.cy.ts:

describe('Homepage', () => {
  it('loads and shows the hero heading', () => {
    cy.visit('/');
    cy.get('h1').should('contain.text', 'Welcome');
  });
});

Run it from the Cypress UI or the terminal:

npx cypress run

You'll see the test execute in a browser with a live command log on the left side.

Understanding the Command Structure

Cypress commands are chainable and automatically retry until the assertion passes or the timeout expires.

cy
  .get('.submit-button')     // find element
  .should('be.visible')      // assert visibility
  .click()                   // interact
  .then(() => {              // run code after click
    cy.url().should('include', '/dashboard');
  });

No await needed — Cypress manages async internally.

Selecting Elements

By text content:

cy.contains('Sign in').click();
cy.contains('h2', 'Dashboard').should('be.visible');

By CSS selector:

cy.get('#email').type('user@example.com');
cy.get('.submit-btn').click();

By data attribute (recommended):

Add data-cy attributes to your HTML for test-stable selectors:

<button data-cy="submit-order">Place order</button>
cy.get('[data-cy="submit-order"]').click();

By role (accessibility-friendly):

cy.get('[role="dialog"]').should('be.visible');
cy.get('[aria-label="Close"]').click();

Using data-cy attributes is the Cypress team's recommended approach. They're invisible to users, never break due to CSS or text changes, and clearly communicate testing intent to developers.

Common Assertions

// Visibility
cy.get('.modal').should('be.visible');
cy.get('.error').should('not.exist');

// Text content
cy.get('h1').should('have.text', 'Welcome back');
cy.get('.count').should('contain', '42');

// URL
cy.url().should('include', '/dashboard');
cy.url().should('eq', 'https://app.example.com/profile');

// Form values
cy.get('#name-input').should('have.value', 'Alice');

// Element state
cy.get('button[type="submit"]').should('be.disabled');
cy.get('#checkbox').should('be.checked');

A Real-World Test: Signup Flow

describe('Signup', () => {
  beforeEach(() => {
    cy.visit('/signup');
  });

  it('creates an account with valid data', () => {
    cy.get('[data-cy="name"]').type('Alice Smith');
    cy.get('[data-cy="email"]').type('alice@example.com');
    cy.get('[data-cy="password"]').type('Str0ngP@ss!');
    cy.get('[data-cy="confirm-password"]').type('Str0ngP@ss!');
    cy.get('[data-cy="submit"]').click();

    cy.url().should('include', '/onboarding');
    cy.get('[data-cy="welcome-message"]').should('contain', 'Alice');
  });

  it('shows error for mismatched passwords', () => {
    cy.get('[data-cy="password"]').type('Password1!');
    cy.get('[data-cy="confirm-password"]').type('Password2!');
    cy.get('[data-cy="submit"]').click();

    cy.get('[data-cy="password-error"]')
      .should('be.visible')
      .and('contain', 'Passwords do not match');
  });
});

Handling Authentication

Logging in before every test is slow. Use cy.session() to save and restore browser state:

// cypress/support/commands.ts
Cypress.Commands.add('login', (email: string, password: string) => {
  cy.session([email, password], () => {
    cy.visit('/login');
    cy.get('[data-cy="email"]').type(email);
    cy.get('[data-cy="password"]').type(password);
    cy.get('[data-cy="submit"]').click();
    cy.url().should('include', '/dashboard');
  });
});

Declare the type in cypress/support/index.d.ts:

declare namespace Cypress {
  interface Chainable {
    login(email: string, password: string): Chainable<void>;
  }
}

Use in tests:

beforeEach(() => {
  cy.login('user@example.com', 'password');
});

cy.session() saves cookies, localStorage, and sessionStorage. Subsequent test runs restore the session without re-running the login flow, unless the session is invalidated.

Viewing Test Results

Interactive mode (npx cypress open): Click any command in the left panel to see a snapshot of the DOM at that point. Hover to see before/after states.

Headless mode (npx cypress run): Generates a video and screenshots on failure in cypress/videos/ and cypress/screenshots/.

Running Specific Tests

# Run a specific spec file
npx cypress run --spec <span class="hljs-string">"cypress/e2e/auth.cy.ts"

<span class="hljs-comment"># Run all specs matching a pattern
npx cypress run --spec <span class="hljs-string">"cypress/e2e/checkout/**"

<span class="hljs-comment"># Run in a specific browser
npx cypress run --browser firefox

Common Mistakes to Avoid

Don't use cy.wait() with fixed timeouts:

// Bad
cy.wait(3000);

// Good
cy.get('[data-cy="loading-complete"]').should('be.visible');

Don't assert on elements that aren't ready: Cypress retries assertions automatically, but only for the element in the chain. Use .should() chains rather than storing element references.

Don't use cy.get() without unique selectors: Multiple matching elements cause unexpected behavior. Use data-cy attributes or .eq(n) to target specific elements.

What to Learn Next

Once you're comfortable with the basics:

  • cy.intercept() — mock API calls without a real backend
  • Component testing — test React/Vue components in isolation
  • Custom commands — reuse complex interactions across tests
  • CI integration — run tests in GitHub Actions with parallelization

Pair Cypress With Continuous Monitoring

Running tests locally is just the start. For continuous monitoring — executing your tests every 5 minutes and alerting when something breaks — HelpMeTest runs your suite 24/7 with no infrastructure to manage.

Summary

You've written your first Cypress test. The fundamentals:

  1. Install with npm install --save-dev cypress
  2. Set baseUrl in cypress.config.ts
  3. Use cy.visit(), cy.get(), and .should() for basic tests
  4. Use data-cy attributes for stable selectors
  5. Use cy.session() for fast, reusable authentication
  6. Run headlessly with npx cypress run for CI

From here, the Cypress docs are excellent. Every edge case is covered with real examples.

Read more