End-to-end (E2E) testing is essential to ensure that your web application works as expected by simulating real user interactions. Cypress is one of the most popular tools for writing and running end-to-end tests for web applications. It provides an easy-to-use and fast way to automate browser interactions and validate the behavior of your app from a user’s perspective.
1. What is Cypress?
Cypress is a JavaScript-based testing framework that is specifically designed for end-to-end testing. Unlike traditional testing tools that interact with the browser in the background, Cypress runs directly in the browser, enabling you to write tests that simulate real user actions such as clicking buttons, typing in forms, navigating pages, and more.
2. Setting Up Cypress
Installation
To get started with Cypress, you need to install it as a development dependency in your project:
npm install --save-dev cypress
Once installed, you can open Cypress with the following command:
npx cypress open
This will launch the Cypress Test Runner, which allows you to interactively run tests and view results in real-time.
Folder Structure
By default, Cypress creates the following directory structure when you run npx cypress open
for the first time:
/cypress
/fixtures # Mock data for tests
/integration # Test files go here
/plugins # Cypress plugins (e.g., extending functionality)
/support # Custom commands and setup
Test files are placed in the /integration
folder.
3. Writing Your First Test
Cypress tests are typically written in JavaScript. Here’s an example of how to write a simple end-to-end test for a login form.
Example: Testing a Login Form
Consider a simple login form with a username, password field, and a login button:
<form id="login-form">
<input type="text" id="username" />
<input type="password" id="password" />
<button type="submit" id="login-button">Login</button>
</form>
Now, let’s write a test to simulate entering credentials and submitting the form.
Test: login.spec.js
describe('Login Form', () => {
it('successfully logs in with valid credentials', () => {
// Visit the page with the login form
cy.visit('http://localhost:3000/login');
// Type into username and password fields
cy.get('#username').type('user123');
cy.get('#password').type('password123');
// Click the login button
cy.get('#login-button').click();
// Check if redirected to the dashboard
cy.url().should('include', '/dashboard');
cy.get('h1').should('contain', 'Welcome to your Dashboard');
});
it('shows error with invalid credentials', () => {
// Visit the page with the login form
cy.visit('http://localhost:3000/login');
// Type invalid credentials
cy.get('#username').type('invalidUser');
cy.get('#password').type('wrongPassword');
// Click the login button
cy.get('#login-button').click();
// Check for error message
cy.get('.error').should('be.visible').and('contain', 'Invalid credentials');
});
});
4. Understanding the Test
cy.visit()
: Opens the URL where the application is hosted (in this case,http://localhost:3000/login
).cy.get()
: Selects elements by CSS selectors (e.g.,#username
or#login-button
).cy.type()
: Simulates typing into an input field.cy.click()
: Simulates a click on an element (like a button).cy.url()
: Asserts that the URL has changed after a successful login.cy.get().should()
: Asserts that the selected element is in the correct state (e.g., checking if an error message is visible).
5. Running the Tests
To run Cypress tests, you can use the Test Runner UI that opens when you run npx cypress open
or run the tests headlessly using:
npx cypress run
This will run all tests without opening the browser. Cypress will automatically capture a video and generate a screenshot for each failed test.
6. Best Practices for E2E Testing with Cypress
Here are some best practices for writing robust and maintainable E2E tests with Cypress:
a) Use Fixtures for Mock Data
You can mock data to test your application without relying on real API calls. Cypress provides a fixtures folder where you can store mock JSON data. Here’s how to use it in tests.
Example: Using a mock user for login:
it('logs in with mocked credentials', () => {
cy.fixture('user.json').then((user) => {
cy.get('#username').type(user.username);
cy.get('#password').type(user.password);
cy.get('#login-button').click();
cy.url().should('include', '/dashboard');
});
});
user.json
could look like:
{
"username": "user123",
"password": "password123"
}
b) Keep Tests Isolated
Each test should be independent. Ensure that one test’s success or failure doesn’t affect another test. This can be achieved by resetting the state before each test using beforeEach()
.
beforeEach(() => {
cy.visit('http://localhost:3000/login');
});
c) Use Custom Commands
Cypress allows you to define custom commands for repetitive tasks, such as logging in. This helps keep your tests clean and maintainable.
// In support/commands.js
Cypress.Commands.add('login', (username, password) => {
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('#login-button').click();
});
// In your tests
it('logs in with valid credentials', () => {
cy.login('user123', 'password123');
cy.url().should('include', '/dashboard');
});
d) Avoid Using Time-Based Assertions
Avoid relying on hard-coded cy.wait()
or setTimeout()
for waiting. Instead, use Cypress commands like cy.get()
with .should()
that wait automatically for elements to appear.
cy.get('.welcome-message').should('be.visible');
This ensures that the test will wait for the element to appear before continuing.
e) Test on Multiple Browsers
Cypress supports multiple browsers (Chrome, Firefox, Electron). You can run tests on different browsers to ensure compatibility.
npx cypress run --browser firefox
f) Headless Testing for CI/CD
For continuous integration (CI) pipelines, it’s essential to run tests in headless mode (without opening the browser window). This can be done using the --headless
flag:
npx cypress run --headless
7. Mocking Network Requests
Cypress can also intercept and mock network requests, which is particularly useful when you want to isolate your tests from external dependencies like APIs.
Example: Mocking API Response
it('loads user data from API', () => {
cy.intercept('GET', '/api/user', { fixture: 'user.json' }).as('getUser');
cy.visit('/profile');
cy.wait('@getUser');
cy.get('.username').should('contain', 'user123');
});
8. Running Tests in CI/CD Pipelines
Cypress can be easily integrated into continuous integration and deployment (CI/CD) pipelines like GitHub Actions, CircleCI, or Jenkins. To run Cypress tests in CI, use the cypress
command within your pipeline configuration.
Example GitHub Action:
name: Cypress Tests
on:
push:
branches:
- main
jobs:
cypress:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install dependencies
run: npm install
- name: Run Cypress tests
run: npx cypress run --headless