Table of Contents
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
| Feature | Cypress | Selenium |
|---|---|---|
| Execution Speed | Fast (runs in-browser) | Slower (out-of-process) |
| Auto-Waiting | Built-in | Manual waits |
| Debugging | Time-travel debugger | Log-based debugging |
| Network Interception | Built-in cy.intercept() | Requires external proxy |
| Setup Complexity | Zero config | Driver management |
| Screenshots/Video | Automatic | Manual 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')
})
})
})
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
},
})