Back to Blog

Technical Blog

Articles on testing, frontend development, and engineering best practices.

Complete Guide to Cypress Testing in 2026

From zero to production-ready end-to-end tests — a hands-on guide with real-world examples and best practices.

1. Why Cypress?

Cypress is a modern E2E (end-to-end) testing framework built for the modern web. Unlike Selenium, Cypress runs inside the browser, giving you real-time debugging, automatic waiting, and reliable selectors.

Key Advantages Over Selenium

FeatureCypressSelenium
Execution SpeedFast (runs in-browser)Slower (out-of-process)
Auto-WaitingBuilt-inManual waits
DebuggingTime-travel debuggerLog-based debugging
Network InterceptionBuilt-in cy.intercept()Requires external proxy
Setup ComplexityZero configDriver management
Screenshots/VideoAutomaticManual setup
When to use Cypress: Best for single-page applications (SPAs), React/Angular/Vue projects, and teams wanting fast, reliable E2E tests with minimal setup.

2. Setting Up Cypress

Getting Cypress up and running takes under 5 minutes.

Installation

# Navigate to your project
cd your-react-app

# Install Cypress as a dev dependency
npm install cypress --save-dev

# Open Cypress for the first time
npx cypress open

When you run cypress open for the first time, Cypress creates a cypress/ folder with example tests and configuration:

your-project/
├── cypress/
│   ├── e2e/              # Your test files go here
│   │   └── home.cy.js
│   ├── fixtures/         # Test data (JSON files)
│   ├── support/          # Custom commands, hooks
│   │   ├── commands.js
│   │   └── e2e.js
│   └── downloads/
├── cypress.config.js     # Cypress configuration
└── package.json

cypress.config.js

const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',  // Your app's URL
    viewportWidth: 1280,
    viewportHeight: 720,
    video: true,
    screenshotOnRunFailure: true,
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
})

Adding Scripts to package.json

{
  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "cy:headless": "cypress run --headless --browser chrome"
  }
}

3. Writing Your First Test

Let's create a test for a typical React portfolio page.

Basic Test Structure

// cypress/e2e/home.cy.js

describe('Homepage', () => {
  beforeEach(() => {
    cy.visit('/')
  })

  it('loads the page successfully', () => {
    cy.title().should('include', 'Avinash Bawage')
    cy.get('body').should('be.visible')
  })

  it('displays the hero section', () => {
    cy.get('.hero-name')
      .should('be.visible')
      .and('contain', 'Avinash Bawage')
  })

  it('has working navigation links', () => {
    cy.get('.nav-links a').should('have.length.greaterThan', 3)
    cy.get('.nav-links a').each(($link) => {
      cy.wrap($link).should('have.attr', 'href')
    })
  })

  it('scrolls to contact section when "Hire Me" is clicked', () => {
    cy.get('.nav-cta').click()
    cy.url().should('include', '#contact')
    cy.get('#contact').should('be.visible')
  })

  it('submits the contact form', () => {
    cy.get('#fullname').type('John Doe')
    cy.get('#email').type('john@example.com')
    cy.get('#subject').type('Test Inquiry')
    cy.get('#message').type('Hello from Cypress!')
    cy.get('#frmContact').submit()
    cy.on('window:alert', (text) => {
      expect(text).to.contain('Message sent')
    })
  })
})
Cypress best practice: Use data-testid attributes for stable selectors instead of CSS classes that may change with styling updates.

4. Selectors & Commands

Cypress provides powerful selectors and chainable commands for interacting with elements.

Querying Elements

// By CSS selector
cy.get('button.submit')

// By data-testid (recommended!)
cy.get('[data-testid="login-button"]')

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

// By role (accessibility-first)
cy.get('button').contains('Submit')

// Finding child elements
cy.get('.form-group')
  .first()
  .find('input')
  .type('Hello')

// Chaining
cy.get('#contact')
  .find('form')
  .should('exist')
  .within(() => {
    cy.get('input').first().type('Name')
  })

Actions

// Click
cy.get('.btn-primary').click()
cy.get('.btn-primary').click({ force: true })  // Override actionability

// Type
cy.get('input').type('Hello World')
cy.get('input').type('{selectall}New text')
cy.get('input').type('text', { delay: 100 })

// Clear
cy.get('input').clear()

// Select dropdown
cy.get('select').select('option-value')

// Check/Uncheck
cy.get('input[type="checkbox"]').check()
cy.get('input[type="checkbox"]').uncheck()

// Scroll
cy.get('#section').scrollIntoView()

Assertions

// Visibility
cy.get('.alert').should('be.visible')
cy.get('.spinner').should('not.exist')

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

// Attribute
cy.get('a').should('have.attr', 'href', '/login')

// Class
cy.get('button').should('have.class', 'active')

// Value
cy.get('input').should('have.value', 'John')

// Length
cy.get('.list-item').should('have.length', 5)

// Style
cy.get('.btn').should('have.css', 'background-color', 'rgb(99, 102, 241)')

5. Intercepting API Calls

Cypress can intercept and mock network requests, letting you test your UI without a real backend.

Basic Intercept

describe('API Interception', () => {
  it('intercepts a GET request', () => {
    // Intercept and alias
    cy.intercept('GET', '/api/projects').as('getProjects')

    cy.visit('/projects')
    cy.wait('@getProjects').then((interception) => {
      expect(interception.response.statusCode).to.eq(200)
    })
  })

  it('mocks API response', () => {
    cy.intercept('GET', '/api/projects', {
      statusCode: 200,
      body: [
        { id: 1, name: 'Project A' },
        { id: 2, name: 'Project B' },
      ],
    }).as('mockProjects')

    cy.visit('/projects')
    cy.wait('@mockProjects')
    cy.get('.project-card').should('have.length', 2)
  })

  it('simulates slow network', () => {
    cy.intercept('GET', '/api/data', (req) => {
      req.reply((res) => {
        res.send({ delay: 2000, body: res.body })
      })
    })
  })

  it('returns error status', () => {
    cy.intercept('POST', '/api/contact', {
      statusCode: 500,
      body: { error: 'Server error' },
    })

    cy.get('#frmContact').submit()
    cy.get('.error-message').should('contain', 'Something went wrong')
  })
})

6. Working with Fixtures

Fixtures let you load test data from external JSON files.

cypress/fixtures/users.json

{
  "valid": {
    "name": "John Doe",
    "email": "john@example.com",
    "subject": "Test Inquiry",
    "message": "Hello from Cypress!"
  },
  "invalid": {
    "name": "",
    "email": "not-an-email",
    "subject": "",
    "message": ""
  }
}

Using Fixtures in Tests

describe('Contact Form', () => {
  beforeEach(function () {
    cy.fixture('users').as('users')
  })

  it('submits with valid data', function () {
    cy.get('#fullname').type(this.users.valid.name)
    cy.get('#email').type(this.users.valid.email)
    cy.get('#subject').type(this.users.valid.subject)
    cy.get('#message').type(this.users.valid.message)
    cy.get('#frmContact').submit()
  })

  it('rejects invalid email', function () {
    cy.get('#email').type(this.users.invalid.email)
    cy.get('#email').should('have.value', 'not-an-email')
    // HTML5 validation should prevent submit
  })
})

7. Custom Commands

Reusable commands keep your tests DRY (Don't Repeat Yourself).

cypress/support/commands.js

// Custom command: Login
Cypress.Commands.add('login', (email, password) => {
  cy.session([email, password], () => {
    cy.visit('/login')
    cy.get('#email').type(email)
    cy.get('#password').type(password)
    cy.get('button[type="submit"]').click()
    cy.url().should('not.include', '/login')
  })
})

// Custom command: Fill contact form
Cypress.Commands.add('fillContactForm', (data) => {
  cy.get('#fullname').type(data.name)
  cy.get('#email').type(data.email)
  cy.get('#subject').type(data.subject)
  cy.get('#message').type(data.message)
})

// Custom command: Check if element is in viewport
Cypress.Commands.add('isInViewport', { prevSubject: true }, (subject) => {
  const rect = subject[0].getBoundingClientRect()
  expect(rect.top).to.be.lessThan(Cypress.config('viewportHeight'))
  expect(rect.bottom).to.be.greaterThan(0)
  return subject
})

// Usage in tests
describe('Portfolio', () => {
  it('uses custom commands', () => {
    cy.visit('/')
    cy.fillContactForm({
      name: 'Test User',
      email: 'test@example.com',
      subject: 'Hello',
      message: 'Testing!'
    })
  })
})

8. CI/CD Integration

Run Cypress tests automatically on every push using GitHub Actions.

.github/workflows/cypress.yml

name: Cypress Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 18

      - name: Install dependencies
        run: npm ci

      - name: Start dev server
        run: npm start &
        env:
          CI: true

      - name: Wait for server
        run: npx wait-on http://localhost:3000

      - name: Run Cypress tests
        run: npx cypress run

      - name: Upload screenshots
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots

9. Best Practices

Use data-testid Selectors

// Instead of this (fragile)
cy.get('.btn-primary > span')

// Do this (stable)
cy.get('[data-testid="submit-btn"]')

Use cy.intercept() to Mock APIs

// Don't rely on real backend in E2E tests
cy.intercept('GET', '/api/projects', { fixture: 'projects.json' })

Use cy.session() for Authentication

// Cache login across tests in the same describe block
cy.session([email, password], () => {
  cy.visit('/login')
  cy.login(email, password)
})

Don't Test What You Don't Own

Avoid testing third-party libraries — focus on your application's behavior and user flows.

Run Tests in Parallel

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    experimentalRunAllSpecs: true,  // For Cypress < 13
  },
})
Back to Top