How to Write Cypress Tests
Cypress tests are JavaScript files that describe user interactions and verify application behavior. Each test simulates what a real user would do, clicking buttons, filling forms, navigating pages, and checking that the right content appears. The framework handles browser management, waiting, and retries automatically, so you can focus on describing the behavior you want to verify.
Step 1: Structure Your Test File
Cypress uses the Mocha test framework's describe and it pattern. Each test file typically covers a single feature or page of your application. The describe block groups related tests, and each it block defines one specific behavior to verify:
describe('Product Catalog', () => {
beforeEach(() => {
cy.visit('/products');
});
it('displays the product list', () => {
cy.get('.product-card').should('have.length.greaterThan', 0);
});
it('filters products by category', () => {
cy.get('[data-testid="category-filter"]').select('Electronics');
cy.get('.product-card').each(($card) => {
cy.wrap($card).should('contain', 'Electronics');
});
});
it('opens product details when clicked', () => {
cy.get('.product-card').first().click();
cy.url().should('include', '/products/');
cy.get('.product-details').should('be.visible');
});
});
The beforeEach hook runs before every test in the describe block, providing a consistent starting point. Use it for navigation, login, or any setup that every test in the group needs. Avoid putting assertions in beforeEach since its purpose is setup, not verification.
Name your it blocks descriptively so that test reports clearly communicate what each test verifies. Good names start with a verb: "displays the product list," "filters products by category," "shows an error when the API fails." When a test fails in CI, the name should tell you exactly what broke without needing to read the test code.
Step 2: Navigate and Interact with Pages
Every test typically starts with cy.visit() to load a page. If you have configured a baseUrl in cypress.config.js, you can use relative paths:
cy.visit('/login'); // Goes to baseUrl + /login
cy.visit('/products?page=2'); // Query parameters work too
Once on a page, use cy.get() to select elements and chain action commands to interact with them:
// Click a button
cy.get('[data-testid="submit"]').click();
// Type into an input
cy.get('input[name="email"]').type('user@example.com');
// Clear and retype
cy.get('input[name="search"]').clear().type('new query');
// Select a dropdown option
cy.get('select[name="country"]').select('United States');
// Check and uncheck checkboxes
cy.get('#terms-checkbox').check();
cy.get('#newsletter-checkbox').uncheck();
// Scroll to an element
cy.get('#footer').scrollIntoView();
The cy.contains() command finds elements by their visible text content, which is especially useful for buttons and links where the text label is the most stable identifier:
cy.contains('Add to Cart').click();
cy.contains('a', 'View Details').click(); // Restrict to anchor tags
Cypress automatically waits for elements to be actionable before performing clicks, types, and other interactions. If an element is covered by a loading overlay, Cypress waits for the overlay to disappear before clicking. If an element is not yet in the DOM, Cypress retries the query until it appears.
Step 3: Write Effective Assertions
Assertions verify that the application is in the expected state after an interaction. Chain .should() onto any Cypress command to add assertions:
// Visibility and existence
cy.get('.success-message').should('be.visible');
cy.get('.error-alert').should('not.exist');
// Text content
cy.get('h1').should('have.text', 'Dashboard');
cy.get('.greeting').should('contain', 'Welcome');
// Attributes and classes
cy.get('button').should('have.attr', 'disabled');
cy.get('.tab').should('have.class', 'active');
// Numeric comparisons
cy.get('.cart-items').should('have.length', 3);
cy.get('.total').invoke('text').then(parseFloat)
.should('be.greaterThan', 0);
// URL and page state
cy.url().should('include', '/dashboard');
cy.title().should('contain', 'My App');
Every .should() assertion automatically retries. Cypress re-queries the DOM and re-evaluates the assertion until it passes or the timeout expires (default four seconds). This means you do not need to add waits between an action and its expected result. If clicking a button triggers an API call that updates the page, just assert on the expected outcome and Cypress waits for it to happen.
For assertions that need custom logic, use .then() to access the element directly:
cy.get('.price').then(($el) => {
const price = parseFloat($el.text().replace('$', ''));
expect(price).to.be.within(10, 100);
});
Step 4: Use Fixtures for Test Data
Fixtures are JSON files stored in cypress/fixtures/ that contain test data. They keep your test files clean by separating data from test logic, and they make test data reusable across multiple tests:
// cypress/fixtures/user.json
{
"email": "testuser@example.com",
"password": "securePassword123",
"name": "Test User"
}
// In your test file
describe('User Profile', () => {
beforeEach(() => {
cy.fixture('user').as('userData');
});
it('updates the user name', function () {
cy.visit('/profile');
cy.get('[data-testid="name-input"]')
.clear()
.type(this.userData.name);
cy.get('[data-testid="save-btn"]').click();
cy.get('.success').should('contain', 'Profile updated');
});
});
Fixtures are also used with cy.intercept() to provide stubbed API responses. Store the expected API response as a fixture file and reference it in your intercept call, keeping network stubs organized and maintainable.
Step 5: Stub Network Requests
The cy.intercept() command lets you control network requests, replacing real API calls with stubbed responses. This is essential for testing error states, loading indicators, and edge cases that are difficult to reproduce with a live backend:
// Stub a successful API response
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: [
{ id: 1, name: 'Widget', price: 29.99 },
{ id: 2, name: 'Gadget', price: 49.99 }
]
}).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.get('.product-card').should('have.length', 2);
// Stub an error response to test error handling
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { error: 'Internal server error' }
}).as('getProductsError');
cy.visit('/products');
cy.wait('@getProductsError');
cy.get('.error-message').should('contain', 'Something went wrong');
// Stub with a fixture file
cy.intercept('GET', '/api/users/me', { fixture: 'currentUser.json' });
You can also use cy.intercept() without stubbing to spy on requests and assert that they were made with the expected parameters:
cy.intercept('POST', '/api/orders').as('createOrder');
cy.get('[data-testid="checkout-btn"]').click();
cy.wait('@createOrder').its('request.body')
.should('have.property', 'items')
.and('have.length', 3);
Step 6: Create Custom Commands
Custom commands extract reusable test logic into named functions that you can call anywhere in your test suite. Define them in cypress/support/commands.js:
// Login command that bypasses the UI for speed
Cypress.Commands.add('loginByApi', (email, password) => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password }
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token);
});
});
// Use in tests
beforeEach(() => {
cy.loginByApi('test@example.com', 'password123');
cy.visit('/dashboard');
});
The API-based login command above is significantly faster than logging in through the UI in every test. It sends a direct HTTP request to the login endpoint, stores the resulting token, and bypasses the login page entirely. This pattern is recommended for tests that need authentication but are not testing the login flow itself.
Other common custom commands include selecting elements by data-testid, filling multi-step forms, and asserting on toast notifications:
Cypress.Commands.add('getByTestId', (testId) => {
return cy.get(`[data-testid="${testId}"]`);
});
Cypress.Commands.add('fillAddress', (address) => {
cy.getByTestId('street').type(address.street);
cy.getByTestId('city').type(address.city);
cy.getByTestId('state').select(address.state);
cy.getByTestId('zip').type(address.zip);
});
Effective Cypress tests combine clear structure with practical patterns: use descriptive test names, prefer data-testid selectors, let assertions auto-retry, keep test data in fixtures, stub network requests for isolation, and extract reusable logic into custom commands. Start simple and add complexity only when your testing needs require it.