The single highest-impact optimisation for a Cypress test suite is replacing UI-driven setup with API-driven setup. When your test needs an order to exist before verifying the order details page, creating that order via cy.request('POST', '/api/orders', data) takes milliseconds. Creating it by navigating through the shop, adding items, filling checkout forms, and submitting — the UI route — takes 10-20 seconds. Across 50 tests, that difference is 8 minutes versus 2 seconds of setup time.
API-Driven Setup — Fast, Reliable Test Preconditions
The principle is simple: use the UI only for the feature you are testing. Set up everything else via API.
// ── PATTERN: API setup + UI verification ──
describe('Order Details Page', () => {
let orderId: string;
beforeEach(() => {
// Step 1: Authenticate via API (not UI login form)
cy.apiLogin('standard_user', 'secret_sauce');
// Step 2: Create test order via API (not checkout flow)
cy.request('POST', '/api/orders', {
items: [
{ productId: 1, name: 'Backpack', price: 29.99, quantity: 1 },
{ productId: 2, name: 'Bike Light', price: 9.99, quantity: 2 },
],
shipping: { method: 'standard', cost: 5.99 },
}).then((response) => {
expect(response.status).to.eq(201);
orderId = response.body.id;
});
});
it('should display order with correct item count', () => {
// UI verification — this is what we are ACTUALLY testing
cy.visit(`/orders/${orderId}`);
cy.get('[data-cy="order-items"]').should('have.length', 2);
});
it('should display correct total with shipping', () => {
cy.visit(`/orders/${orderId}`);
// 29.99 + (9.99 * 2) + 5.99 = 55.96
cy.get('[data-cy="order-total"]').should('contain.text', '$55.96');
});
it('should show pending status for new orders', () => {
cy.visit(`/orders/${orderId}`);
cy.get('[data-cy="order-status"]').should('contain.text', 'Pending');
});
});
// ── Common API setup patterns ──
// Create a user with specific attributes
Cypress.Commands.add('createTestUser', (overrides = {}) => {
const defaults = {
name: `TestUser_${Date.now()}`,
email: `test_${Date.now()}@example.com`,
password: 'TestPass1!',
role: 'customer',
};
const userData = { ...defaults, ...overrides };
return cy.request('POST', '/api/test/users', userData)
.its('body');
});
// Seed a product catalogue
Cypress.Commands.add('seedProducts', (count: number = 5) => {
const products = Array.from({ length: count }, (_, i) => ({
name: `Test Product ${i + 1}`,
price: parseFloat((Math.random() * 100 + 1).toFixed(2)),
inStock: true,
}));
return cy.request('POST', '/api/test/seed-products', { products })
.its('body');
});
// Reset the database to a known state
Cypress.Commands.add('resetDatabase', () => {
return cy.request('POST', '/api/test/reset');
});
// ── Usage in tests ──
/*
describe('Product Catalogue', () => {
beforeEach(() => {
cy.resetDatabase();
cy.seedProducts(10);
cy.apiLogin('standard_user', 'secret_sauce');
});
it('should display all seeded products', () => {
cy.visit('/shop');
cy.get('.product-card').should('have.length', 10);
});
});
*/
// ── Setup time comparison ──
const SETUP_COMPARISON = {
'Login via UI': '3-5 seconds (visit, type, click, wait for redirect)',
'Login via API': '50-200 milliseconds (single POST request)',
'Create order via UI': '10-20 seconds (5 pages of navigation and forms)',
'Create order via API':'100-300 milliseconds (single POST request)',
'Suite: 50 tests': 'UI setup: 5-15 min overhead | API setup: 5-10 sec overhead',
};
...overrides pattern in createTestUser is essential for flexible test data. Default values ensure the command works without arguments (cy.createTestUser()), while overrides let specific tests customise what they need: cy.createTestUser({ role: 'admin' }). This pattern avoids both hardcoded data (inflexible) and required parameters (verbose). Every API setup command should provide sensible defaults with optional overrides.`test_${Date.now()}@example.com`. When tests run in parallel or are retried without cleanup, fixed data like test@example.com causes “email already exists” errors. Timestamped data is unique per invocation and eliminates this entire category of parallel execution failures.Common Mistakes
Mistake 1 — Using UI-driven setup for every test precondition
❌ Wrong: Every test navigates through 5 pages to create an order before testing the order details page — 50 tests waste 10+ minutes on repeated checkout flows.
✅ Correct: cy.request('POST', '/api/orders', data) in beforeEach() — order created in milliseconds, test focuses on verifying the details page.
Mistake 2 — Using fixed test data that conflicts in parallel execution
❌ Wrong: Every test creates a user with email test@example.com — parallel tests get “email already exists” errors.
✅ Correct: email: `test_${Date.now()}@example.com` — unique per invocation, no conflicts.