
Cypress Testing Complete Guide
Cypress changed how developers think about testing. Running directly in the browser with automatic waiting, time-travel debugging, and real-time reloading, Cypress eliminates the pain points that made end-to-end testing frustrating. If you've struggled with flaky Selenium tests, Cypress will feel like a breath of fresh air.
This guide covers everything from initial setup to advanced patterns used in production applications.
Table Of Contents-
Why Cypress?
Cypress is a JavaScript-based end-to-end testing framework built for modern web applications. Unlike Selenium, which operates outside the browser, Cypress runs directly inside the browser alongside your application.
Key Advantages
| Feature | Benefit |
|---|---|
| Automatic Waiting | No manual waits - Cypress waits for elements, animations, and API calls |
| Time Travel | Hover over commands to see exactly what happened at each step |
| Real-time Reloading | Tests re-run automatically when you save changes |
| In-Browser Debugging | Use Chrome DevTools while tests run |
| Network Control | Stub and spy on network requests easily |
| Screenshots & Videos | Automatic capture on failure |
| Consistent Results | Runs in the same browser loop, eliminating flakiness |
Cypress vs Selenium
| Aspect | Cypress | Selenium |
|---|---|---|
| Architecture | Runs in browser | External WebDriver |
| Language | JavaScript/TypeScript | Multi-language |
| Setup | npm install | Drivers + bindings |
| Waiting | Automatic | Manual waits needed |
| Debugging | Time travel, snapshots | Limited |
| Speed | Fast | Slower |
| Browser Support | Chrome, Firefox, Edge, Electron | All browsers |
| Mobile | No native support | Via Appium |
When to Choose Cypress: Use Cypress for modern JavaScript applications where you need fast feedback, excellent debugging, and don't require cross-browser testing beyond Chrome-based browsers.
Installation and Setup
Prerequisites
- Node.js 18.x or higher
- npm or yarn
- Modern browser (Chrome, Firefox, Edge)
Install Cypress
# Create new project (if needed)
mkdir cypress-tests && cd cypress-tests
npm init -y
# Install Cypress
npm install cypress --save-dev
# Or with yarn
yarn add cypress --devOpen Cypress
# First run opens Cypress Launchpad
npx cypress openThe Launchpad guides you through:
- Choosing testing type (E2E or Component)
- Selecting a browser
- Creating initial configuration
Quick Setup with TypeScript
npm install typescript --save-devCypress automatically detects TypeScript and creates tsconfig.json in the cypress folder.
Project Structure
After initialization, Cypress creates this structure:
project/
├── cypress/
│ ├── e2e/ # Test files
│ │ └── spec.cy.js
│ ├── fixtures/ # Test data (JSON)
│ │ └── example.json
│ ├── support/ # Custom commands and setup
│ │ ├── commands.js # Custom commands
│ │ └── e2e.js # Support file (loads before tests)
│ └── downloads/ # Downloaded files (gitignored)
├── cypress.config.js # Cypress configuration
└── package.jsonKey Files
| File | Purpose |
|---|---|
cypress.config.js | Main configuration (base URL, timeouts, etc.) |
cypress/e2e/*.cy.js | Test spec files |
cypress/support/commands.js | Custom commands |
cypress/support/e2e.js | Runs before every test file |
cypress/fixtures/*.json | Static test data |
Writing Your First Test
Basic Test Structure
// cypress/e2e/first-test.cy.js
describe('My First Test Suite', () => {
beforeEach(() => {
// Runs before each test
cy.visit('https://example.com')
})
it('should display the homepage', () => {
// Verify page loaded
cy.contains('Example Domain')
})
it('should have working navigation', () => {
cy.contains('More information').click()
cy.url().should('include', 'iana.org')
})
})Test File Naming
Cypress looks for tests matching these patterns:
cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
Common conventions:
login.cy.js- Feature-based naminglogin.spec.js- Spec-based naming (configure in cypress.config.js)
Running Tests
# Open interactive mode
npx cypress open
# Run all tests headlessly
npx cypress run
# Run specific spec file
npx cypress run --spec "cypress/e2e/login.cy.js"
# Run in specific browser
npx cypress run --browser chrome
# Run with headed browser (visible)
npx cypress run --headedSelecting Elements
Best Practice: Use data-* Attributes
<!-- In your application -->
<button data-cy="submit-btn" class="btn btn-primary">Submit</button>// In your test
cy.get('[data-cy="submit-btn"]').click()Using data-cy or data-testid attributes:
- Decouples tests from CSS/styling changes
- Makes test intent clear
- Survives refactoring
Selector Methods
// By data attribute (recommended)
cy.get('[data-cy="search-input"]')
cy.get('[data-testid="submit-button"]')
// By CSS selector
cy.get('#username') // ID
cy.get('.error-message') // Class
cy.get('input[type="email"]') // Attribute
cy.get('form > button') // Child combinator
// By text content
cy.contains('Submit') // Find by text
cy.contains('button', 'Submit') // Element with text
// Within a container
cy.get('[data-cy="login-form"]').within(() => {
cy.get('input[name="email"]').type('user@test.com')
cy.get('input[name="password"]').type('password')
})
// Find within element
cy.get('.nav').find('a').first()Selector Priority (Recommended)
data-cyordata-testid(most stable)id(if stable and meaningful)nameattribute (for form fields)- CSS class (if semantic, not styling)
- Element + attribute combination
cy.contains()for text (use carefully)
Interacting with Elements
Common Actions
// Click
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="menu"]').click({ force: true }) // Click even if covered
// Type text
cy.get('[data-cy="email"]').type('user@example.com')
cy.get('[data-cy="search"]').type('query{enter}') // Type and press Enter
// Clear and type
cy.get('[data-cy="email"]').clear().type('new@example.com')
// Select from dropdown
cy.get('[data-cy="country"]').select('United States') // By visible text
cy.get('[data-cy="country"]').select('US') // By value
// Check/Uncheck
cy.get('[data-cy="agree"]').check()
cy.get('[data-cy="newsletter"]').uncheck()
// Focus and blur
cy.get('[data-cy="input"]').focus()
cy.get('[data-cy="input"]').blur()
// Scroll
cy.get('[data-cy="footer"]').scrollIntoView()
cy.scrollTo('bottom')
// Hover (trigger mouseover)
cy.get('[data-cy="menu"]').trigger('mouseover')
// Right-click
cy.get('[data-cy="item"]').rightclick()
// Double-click
cy.get('[data-cy="item"]').dblclick()Special Keys
cy.get('[data-cy="input"]').type('{enter}') // Enter key
cy.get('[data-cy="input"]').type('{esc}') // Escape key
cy.get('[data-cy="input"]').type('{backspace}') // Backspace
cy.get('[data-cy="input"]').type('{del}') // Delete
cy.get('[data-cy="input"]').type('{selectall}') // Select all
cy.get('[data-cy="input"]').type('{ctrl+a}') // Ctrl+A
cy.get('[data-cy="input"]').type('{shift+tab}') // Shift+TabFile Upload
// With cypress-file-upload plugin or native
cy.get('[data-cy="file-input"]').selectFile('cypress/fixtures/test-file.pdf')
// Multiple files
cy.get('[data-cy="file-input"]').selectFile([
'cypress/fixtures/file1.pdf',
'cypress/fixtures/file2.pdf',
])
// Drag and drop
cy.get('[data-cy="dropzone"]').selectFile('cypress/fixtures/image.png', {
action: 'drag-drop',
})Assertions
Cypress uses Chai assertions with automatic retry until they pass or timeout.
Implicit Assertions (Chainable)
// Should assertions
cy.get('[data-cy="title"]').should('be.visible')
cy.get('[data-cy="title"]').should('have.text', 'Welcome')
cy.get('[data-cy="input"]').should('have.value', 'test@example.com')
cy.get('[data-cy="button"]').should('be.disabled')
// Chained assertions with .and()
cy.get('[data-cy="link"]')
.should('have.attr', 'href', '/dashboard')
.and('have.class', 'active')
.and('be.visible')
// Negative assertions
cy.get('[data-cy="error"]').should('not.exist')
cy.get('[data-cy="button"]').should('not.be.disabled')Common Should Assertions
| Assertion | Description |
|---|---|
be.visible | Element is visible |
not.exist | Element doesn't exist in DOM |
have.text | Exact text match |
contain | Text contains |
have.value | Input value |
have.attr | Has attribute |
have.class | Has CSS class |
be.checked | Checkbox/radio checked |
be.disabled | Element disabled |
have.length | Number of elements |
Explicit Assertions
// Using expect
cy.get('[data-cy="items"]').then(($items) => {
expect($items).to.have.length(5)
expect($items.first()).to.contain('Item 1')
})
// Using assert
cy.get('[data-cy="count"]')
.invoke('text')
.then((text) => {
const count = parseInt(text)
assert.isAbove(count, 0, 'Count should be positive')
})URL and Page Assertions
// URL assertions
cy.url().should('include', '/dashboard')
cy.url().should('eq', 'https://example.com/dashboard')
// Title
cy.title().should('eq', 'Dashboard | MyApp')
// Location
cy.location('pathname').should('eq', '/users/profile')
cy.location('search').should('include', 'page=2')Handling Waits
Cypress automatically waits for:
- Elements to exist
- Elements to become visible
- Elements to stop animating
- Network requests (when using aliases)
Automatic Waiting
// Cypress waits up to 4 seconds (default) for element to exist
cy.get('[data-cy="button"]').click()
// Increase timeout for slow elements
cy.get('[data-cy="slow-content"]', { timeout: 10000 }).should('be.visible')Wait for Network Requests
// Set up intercept and alias
cy.intercept('GET', '/api/users').as('getUsers')
// Trigger the request
cy.get('[data-cy="load-users"]').click()
// Wait for request to complete
cy.wait('@getUsers')
// Wait and assert on response
cy.wait('@getUsers').then((interception) => {
expect(interception.response.statusCode).to.eq(200)
expect(interception.response.body).to.have.length.greaterThan(0)
})Avoid Arbitrary Waits
// Bad - arbitrary wait
cy.wait(5000)
// Good - wait for specific condition
cy.get('[data-cy="loading"]').should('not.exist')
cy.get('[data-cy="content"]').should('be.visible')
// Good - wait for network request
cy.intercept('POST', '/api/submit').as('submit')
cy.get('[data-cy="submit"]').click()
cy.wait('@submit')Custom Commands
Create reusable commands for common operations.
Define Custom Commands
// cypress/support/commands.js
// Login command
Cypress.Commands.add('login', (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')
})
// API login (faster)
Cypress.Commands.add('loginByApi', (email, password) => {
cy.request('POST', '/api/auth/login', { email, password }).then(
(response) => {
window.localStorage.setItem('token', response.body.token)
},
)
})
// Get by data-cy shorthand
Cypress.Commands.add('getByDataCy', (selector) => {
return cy.get(`[data-cy="${selector}"]`)
})Use Custom Commands
// In your tests
describe('Dashboard', () => {
beforeEach(() => {
cy.loginByApi('user@test.com', 'password123')
cy.visit('/dashboard')
})
it('should display user data', () => {
cy.getByDataCy('user-name').should('contain', 'Test User')
})
})TypeScript Support for Commands
// cypress/support/index.d.ts
declare namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>
loginByApi(email: string, password: string): Chainable<void>
getByDataCy(selector: string): Chainable<JQuery<HTMLElement>>
}
}Configuration
cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/e2e.js',
// Timeouts
defaultCommandTimeout: 4000,
requestTimeout: 5000,
responseTimeout: 30000,
pageLoadTimeout: 60000,
// Viewport
viewportWidth: 1280,
viewportHeight: 720,
// Screenshots and videos
screenshotOnRunFailure: true,
video: true,
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
// Retries
retries: {
runMode: 2, // Retry failed tests in CI
openMode: 0, // No retries in interactive mode
},
// Environment variables
env: {
apiUrl: 'http://localhost:3001/api',
},
setupNodeEvents(on, config) {
// Plugin configuration
},
},
})Environment Variables
// Access in tests
const apiUrl = Cypress.env('apiUrl')
cy.request(`${apiUrl}/users`)
// Set via command line
// npx cypress run --env apiUrl=https://api.example.com
// Set in cypress.env.json (gitignored)
{
"apiUrl": "http://localhost:3001/api",
"adminUser": "admin@test.com"
}Best Practices
1. Use data-* Attributes
<!-- Don't rely on CSS classes -->
<button class="btn btn-blue-lg">Submit</button>
<!-- Use test-specific attributes -->
<button class="btn btn-blue-lg" data-cy="submit-form">Submit</button>2. Don't Use Arbitrary Waits
// Bad
cy.wait(5000)
// Good
cy.intercept('POST', '/api/submit').as('submit')
cy.get('[data-cy="submit"]').click()
cy.wait('@submit')3. Reset State Before Tests
// Clean state before each test, not after
beforeEach(() => {
cy.request('POST', '/api/reset-db')
cy.loginByApi('user@test.com', 'password')
})4. Keep Tests Independent
// Bad - test depends on previous test
it('should create user', () => {
/* ... */
})
it('should see created user', () => {
/* depends on above */
})
// Good - each test is self-contained
it('should create user', () => {
cy.createUser('new@test.com')
cy.visit('/users')
cy.contains('new@test.com')
})5. Use Fixtures for Test Data
// cypress/fixtures/users.json
{
"validUser": {
"email": "test@example.com",
"password": "SecurePass123"
},
"adminUser": {
"email": "admin@example.com",
"password": "AdminPass123"
}
}// In test
cy.fixture('users').then((users) => {
cy.login(users.validUser.email, users.validUser.password)
})6. Bypass UI for Setup
// Slow - using UI for login in every test
beforeEach(() => {
cy.visit('/login')
cy.get('#email').type('user@test.com')
cy.get('#password').type('password')
cy.get('#submit').click()
})
// Fast - use API for login
beforeEach(() => {
cy.loginByApi('user@test.com', 'password')
})Test Your Knowledge
Quiz on Cypress Testing
Your Score: 0/10
Question: What is the key architectural difference between Cypress and Selenium?
Continue Your Cypress Journey
Frequently Asked Questions
Frequently Asked Questions (FAQs) / People Also Ask (PAA)
Can Cypress test mobile applications?
What browsers does Cypress support?
How do I handle iframes in Cypress?
Why are my Cypress tests flaky?
How do I run Cypress tests in CI/CD?
Can Cypress test applications that require authentication?
What is the difference between cy.request() and cy.intercept()?
How do I debug failing Cypress tests?