Cypress E2E Testing Framework — Practical Guide
Cypress is a developer-friendly end-to-end testing framework that runs tests in the browser alongside your application, providing real-time reloading, time-travel debugging, and automatic waiting for seamless test authoring.
What You’ll Learn
By the end of this tutorial, you’ll be able to set up Cypress, write reliable E2E tests using commands and custom assertions, manage test data with fixtures, intercept and stub network requests, and use the Cypress Dashboard for CI debugging.
Why Cypress Matters
Cypress reimagined E2E testing by running inside the browser — not in a separate process. This gives you access to the DOM, network, and application state in real time. At DodaTech, Cypress validates critical user flows in DodaZIP’s web interface, ensuring file operations work correctly before every deployment.
Cypress Learning Path
flowchart LR
A[Testing Basics] --> B[Jest]
B --> C[Playwright]
C --> D[Cypress]
D --> E{You Are Here}
style E fill:#f90,color:#fff
Setting Up Cypress
Install Cypress in your project:
npm install --save-dev cypress
npx cypress openThis creates the default folder structure:
cypress/
e2e/ # test files
fixtures/ # test data (JSON)
support/ # custom commands, global config
cypress.config.jsFor headless CI runs:
npx cypress runThe configuration file lets you set base URL, viewport, timeouts, and environment variables:
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.js',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
},
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 4000,
});Commands and Assertions
Cypress chains commands using a fluent API. Commands don’t execute immediately — they queue and run sequentially.
// login.cy.js
describe('Login Flow', () => {
it('logs in with valid credentials', () => {
cy.visit('/login');
cy.get('[data-cy=email]').type('user@example.com');
cy.get('[data-cy=password]').type('secret123');
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/dashboard');
cy.get('[data-cy=welcome]').should('contain', 'Welcome back');
});
it('shows error for invalid credentials', () => {
cy.visit('/login');
cy.get('[data-cy=email]').type('wrong@example.com');
cy.get('[data-cy=password]').type('wrongpass');
cy.get('[data-cy=submit]').click();
cy.get('[data-cy=error]')
.should('be.visible')
.and('contain', 'Invalid email or password');
});
});Fixtures for Test Data
Fixtures let you load external test data instead of hardcoding values.
// cypress/fixtures/users.json
{
"valid": {
"email": "alice@example.com",
"password": "password123",
"name": "Alice"
},
"invalid": {
"email": "bob@example.com",
"password": "wrongpass"
}
}// user-profile.cy.js
describe('User Profile', () => {
beforeEach(() => {
cy.fixture('users').as('userData');
});
it('displays user name', function () {
cy.visit('/profile');
cy.get('[data-cy=email]').type(this.userData.valid.email);
cy.get('[data-cy=name]').should('have.value', this.userData.valid.name);
});
});Network Interception
Cypress can intercept and stub HTTP requests to test how your app handles API responses.
// products.cy.js
describe('Product Listing', () => {
it('shows products from API', () => {
cy.intercept('GET', '/api/products', {
fixture: 'products.json',
}).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.get('[data-cy=product-card]').should('have.length.at.least', 1);
});
it('handles API failure gracefully', () => {
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { error: 'Server error' },
}).as('getProductsFail');
cy.visit('/products');
cy.wait('@getProductsFail');
cy.get('[data-cy=error-banner]')
.should('be.visible')
.and('contain', 'Failed to load products');
});
it('shows empty state when no products exist', () => {
cy.intercept('GET', '/api/products', {
body: [],
});
cy.visit('/products');
cy.get('[data-cy=empty-state]').should('be.visible');
});
});Custom Commands
Reduce duplication by creating reusable custom commands:
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
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');
});
});
// usage in tests
it('accesses settings page', () => {
cy.login('user@example.com', 'secret123');
cy.visit('/settings');
cy.get('[data-cy=settings-title]').should('be.visible');
});Cypress Dashboard
The Cypress Dashboard records test results, screenshots, and video recordings from CI runs. Connect your project:
npx cypress run --record --key YOUR_RECORD_KEYThe dashboard shows:
- Pass/fail trends over time
- Test duration breakdowns
- Screenshots and video of failing tests
- Flaky test detection
The Cypress Test Flow
flowchart LR
A[cy.visit] --> B[cy.get Element]
B --> C[cy.type / click]
C --> D[cy.should Assert]
D --> E{Pass?}
E -->|Yes| F[Next Command]
E -->|No| G[Show Error + Screenshot]
Common Cypress Mistakes
1. Using CSS Classes for Selectors
Classes change frequently with CSS frameworks or refactoring. Tests break for non-functional reasons.
Fix: Use data-cy attributes for dedicated test selectors. Cypress recommends this as the most resilient approach.
2. Chaining Too Many Commands
A chain of 10 commands makes failures hard to debug — you can’t tell which step failed.
Fix: Break long chains into named steps with descriptive cy.then callbacks or separate test cases.
3. Forgetting to Use cy.wait for Intercepted Requests
Assertions after an intercepted response run immediately, before the response arrives.
Fix: Use cy.wait('@aliasName') to pause until the intercept fires. Then assert on the UI.
4. Testing Third-Party Sites
Cypress cannot navigate to a different origin within a single test (it throws a cross-origin error).
Fix: Use cy.origin() (available in Cypress 12+) for cross-origin tests, or mock the third-party interaction.
5. Not Using cy.session for Authentication
Logging in before every test is slow and creates unnecessary API calls.
Fix: Use cy.session() to cache authentication state. The login only runs once per unique set of credentials.
6. Ignoring Video and Screenshot Artifacts
When a test fails in CI, Cypress captures video and screenshots automatically. Ignoring these means debugging blindly.
Fix: Configure the project to upload cypress/videos/ and cypress/screenshots/ as CI artifacts.
7. Running Tests Sequentially by Default
Cypress runs tests serially within a file by default, which is slow for large suites.
Fix: Split test files by feature and run them in parallel across CI machines using --parallel.
Practice Questions
1. What is the difference between cy.get and cy.find?
cy.get queries from the root of the document. cy.find queries from the previously yielded element. Use find to scope searches within a container.
2. How do you stub a network request in Cypress?
Use cy.intercept(method, url, response) to intercept matching requests. The response can be a fixture, inline data, or an error status.
3. What is cy.session used for?
It caches browser state (cookies, localStorage, sessionStorage) between tests, avoiding repeated login steps. Sessions are keyed by an array of identifiers.
4. How do you run Cypress in headless mode?
cypress run runs headless by default. cypress open launches the interactive Test Runner with a visible browser.
5. Challenge: Write a test suite for a form with validation.
Create tests for: successful form submission, empty field validation, invalid email format, server error response, and form reset after submission.
Mini Project: E2E Test Suite for a Search Feature
// search.cy.js
describe('Search Feature', () => {
beforeEach(() => {
cy.visit('/search');
});
it('shows search input and button', () => {
cy.get('[data-cy=search-input]').should('be.visible');
cy.get('[data-cy=search-button]').should('be.visible');
});
it('displays search results', () => {
cy.intercept('GET', '/api/search?q=cypress', {
fixture: 'search-results.json',
}).as('search');
cy.get('[data-cy=search-input]').type('cypress');
cy.get('[data-cy=search-button]').click();
cy.wait('@search');
cy.get('[data-cy=result-item]').should('have.length.at.least', 1);
cy.get('[data-cy=result-count]').should('contain', 'results');
});
it('shows no results message', () => {
cy.intercept('GET', '/api/search?q=zzzzzz', {
body: { results: [], total: 0 },
}).as('emptySearch');
cy.get('[data-cy=search-input]').type('zzzzzz');
cy.get('[data-cy=search-button]').click();
cy.wait('@emptySearch');
cy.get('[data-cy=no-results]').should('be.visible');
});
it('shows loading state', () => {
cy.intercept('GET', '/api/search?q=slow', (req) => {
req.on('response', (res) => {
res.setDelay(2000);
});
}).as('slowSearch');
cy.get('[data-cy=search-input]').type('slow');
cy.get('[data-cy=search-button]').click();
cy.get('[data-cy=loading]').should('be.visible');
cy.wait('@slowSearch');
cy.get('[data-cy=loading]').should('not.exist');
});
it('clears results when input is cleared', () => {
cy.intercept('GET', '/api/search?q=test', {
body: { results: [{ title: 'Test Result' }], total: 1 },
}).as('search');
cy.get('[data-cy=search-input]').type('test');
cy.get('[data-cy=search-button]').click();
cy.wait('@search');
cy.get('[data-cy=result-item]').should('be.visible');
cy.get('[data-cy=search-input]').clear();
cy.get('[data-cy=result-item]').should('not.exist');
});
});FAQ
Try It Yourself
- Install Cypress in a project and open the Test Runner with
npx cypress open - Write a test that visits a page and checks the page title
- Add a fixture file and use it in a test
- Intercept an API call and return mock data
- Run all tests headlessly with
npx cypress run
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro