| name | cypress |
| description | [Applies to: **/*.{js,ts,cy.js,cy.ts}] This guide defines definitive best practices for writing maintainable, performant, and reliable Cypress end-to-end tests, leveraging modern patterns and avoiding common pitfalls. |
| source | cursor_mdc |
Cypress Best Practices
Cypress is our go-to for robust E2E testing. Follow these rules to ensure our test suite remains fast, stable, and easy to maintain.
1. Code Organization & Structure
Adhere to the standard Cypress folder structure. This promotes consistency and discoverability.
Rule: Keep a single cypress.config.ts at the root.
Rule: Place E2E tests in cypress/e2e.
Rule: Store reusable commands in cypress/support/commands.ts.
Rule: Use cypress/fixtures for static test data.
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
retries: {
runMode: 2,
openMode: 0,
},
setupNodeEvents(on, config) {
},
},
});
2. Test Isolation
Each test (it block) must be independent and not rely on the state of previous tests. This enables parallelization and makes debugging straightforward.
❌ BAD: Dependent tests
describe('User Flow', () => {
it('should register a user', () => { });
it('should log in the registered user', () => { });
});
✅ GOOD: Isolated tests using beforeEach
describe('User Management', () => {
beforeEach(() => {
cy.apiLogin('testuser@example.com', 'password123');
cy.visit('/dashboard');
});
it('should display user dashboard', () => {
cy.getBySel('dashboard-header').should('contain', 'Welcome, Test User');
});
it('should allow user to update profile', () => {
cy.getBySel('profile-link').click();
cy.getBySel('name-input').type('New Name');
cy.getBySel('save-button').click();
cy.getBySel('success-message').should('be.visible');
});
});
3. Element Selection
Prioritize stable, test-specific attributes. This decouples selectors from styling or content changes.
Rule: Always use data-cy attributes for element selection.
Rule: Use cy.contains() only when the displayed text is critical to the test's purpose.
❌ BAD: Brittle selectors
cy.get('.btn-primary').click();
cy.get('#submitButton').click();
cy.get('div:nth-child(2) > p').should('be.visible');
✅ GOOD: Resilient selectors with data-cy
Cypress.Commands.add('getBySel', (selector: string, ...args: any[]) => {
return cy.get(`[data-cy="${selector}"]`, ...args);
});
cy.getBySel('submit-button').click();
cy.getBySel('user-profile-name').should('contain', 'John Doe');
cy.contains('Submit Order').click();
4. Mocking Strategies
Control external dependencies to ensure fast, reliable, and deterministic tests.
Rule: Use cy.intercept() for all network requests (XHR/Fetch).
Rule: Use cy.fixture() to provide static response data.
Rule: Use cy.stub() for client-side function mocking.
Rule: For server-side setup/teardown, use cy.task() to execute Node.js code.
❌ BAD: Relying on live API responses
it('should load products', () => {
cy.visit('/products');
cy.get('.product-card').should('have.length.gt', 0);
});
✅ GOOD: Intercepting network requests with fixtures
it('should load products from fixture', () => {
cy.intercept('GET', '/api/products', { fixture: 'products.json' }).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.getBySel('product-card').should('have.length', 3);
});
5. Avoiding Arbitrary Waits
Cypress automatically retries assertions. Avoid cy.wait() unless explicitly waiting for a network request to complete.
❌ BAD: Arbitrary waits
cy.get('button').click();
cy.wait(2000);
cy.get('.success-message').should('be.visible');
✅ GOOD: Leveraging Cypress's retry-ability
cy.getBySel('submit-button').click();
cy.getBySel('success-message').should('be.visible');
✅ GOOD: Waiting for specific network requests
cy.intercept('POST', '/api/users', { statusCode: 201 }).as('createUser');
cy.getBySel('register-form').submit();
cy.wait('@createUser').its('response.statusCode').should('eq', 201);
6. Performance & Real-world Scenarios
Optimize test execution and simulate real user interactions effectively.
Rule: Log in programmatically via cy.request() or a custom command in beforeEach instead of using the UI. This is significantly faster.
Rule: Use cy.session() for persistent login state across tests within a spec file, reducing redundant login calls.
Rule: Leverage cy.prompt() (available 2025) for natural language test generation and self-healing selectors.
Cypress.Commands.add('apiLogin', (email, password) => {
cy.session([email, password], () => {
cy.request('POST', '/api/login', { email, password })
.its('body.token')
.then((token) => {
localStorage.setItem('jwt', token);
});
}, {
cacheAcrossSpecs: true
});
});
describe('Authenticated Features', () => {
beforeEach(() => {
cy.apiLogin('user@example.com', 'password123');
cy.visit('/dashboard');
});
});
7. Test Naming & Readability
Write clear, descriptive test names that explain the intent and expected outcome.
❌ BAD: Vague test names
it('test 1', () => { });
it('login works', () => { });
✅ GOOD: Descriptive test names
describe('Authentication', () => {
it('should allow a valid user to log in and redirect to dashboard', () => { });
context('when credentials are invalid', () => {
it('should display an error message for incorrect password', () => { });
});
});
8. Avoiding External Sites
Keep E2E tests focused on our application. If an external origin is unavoidable, use cy.origin().
❌ BAD: Automating external sites directly
cy.visit('https://our-app.com');
cy.get('.external-link').click();
cy.url().should('include', 'external-provider.com');
cy.get('#external-form').type('data');
✅ GOOD: Using cy.origin() for controlled external interactions
it('should handle OAuth redirect', () => {
cy.visit('/login');
cy.getBySel('oauth-login-button').click();
cy.origin('https://external-oauth.com', () => {
cy.get('#username').type('oauth_user');
cy.get('#password').type('oauth_pass');
cy.get('#submit').click();
});
cy.url().should('include', '/dashboard');
});