Hooks and Test Setup — beforeEach, afterEach and Efficient Test Isolation

Tests that share state are tests that fail randomly. Cypress hooks — beforeEach, afterEach, before, after — provide structured setup and teardown that keeps tests isolated. The most impactful pattern is API-driven test setup: instead of clicking through the UI to reach the starting state, use cy.request() or custom API commands to set up data instantly. This makes each test independent, fast, and deterministic.

Hooks, API-Driven Setup and Test Isolation

Proper test setup follows one rule: get to the starting state as fast as possible without going through UI flows that are not the focus of the test.

// ── Cypress hook execution order ──

/*
  before()      → Runs ONCE before all tests in the describe block
  beforeEach()  → Runs before EVERY test
  it()          → The test itself
  afterEach()   → Runs after EVERY test
  after()       → Runs ONCE after all tests in the describe block

  For nested describes:
  before (outer) → before (inner) → beforeEach (outer) → beforeEach (inner)
  → it → afterEach (inner) → afterEach (outer) → ...
*/


// ── Pattern 1: API-driven login in beforeEach ──

describe('Inventory Page', () => {
  beforeEach(() => {
    // Fast: authenticate via API, not UI
    cy.loginAPI('standard_user', 'secret_sauce');
    cy.visit('/inventory.html');
  });

  it('should display 6 products', () => {
    cy.get('.inventory_item').should('have.length', 6);
  });

  it('should sort products by price low to high', () => {
    cy.get('[data-test="product-sort-container"]').select('lohi');
    cy.get('.inventory_item_price').first().should('contain.text', '$7.99');
  });
});


// ── Pattern 2: Seed test data via API before tests ──

describe('Order History', () => {
  beforeEach(() => {
    // Create test data via API — independent of other tests
    cy.request('POST', '/api/test/seed', {
      user: 'testuser',
      orders: [
        { id: 'ORD-001', total: 29.99, status: 'delivered' },
        { id: 'ORD-002', total: 49.99, status: 'shipped' },
      ],
    });

    cy.loginAPI('testuser', 'password123');
    cy.visit('/orders');
  });

  afterEach(() => {
    // Clean up test data — leave no traces for other tests
    cy.request('DELETE', '/api/test/cleanup', { user: 'testuser' });
  });

  it('should display 2 orders', () => {
    cy.get('.order-row').should('have.length', 2);
  });

  it('should show delivered status for first order', () => {
    cy.contains('.order-row', 'ORD-001')
      .find('.status-badge')
      .should('contain.text', 'Delivered');
  });
});


// ── Pattern 3: Reset state between tests (no API available) ──

describe('Shopping Cart', () => {
  beforeEach(() => {
    // Clear cookies and localStorage to reset session state
    cy.clearCookies();
    cy.clearLocalStorage();

    // Login fresh for each test
    cy.loginAPI('standard_user', 'secret_sauce');
    cy.visit('/inventory.html');
  });

  it('should start with empty cart', () => {
    cy.get('.shopping_cart_badge').should('not.exist');
  });

  it('should add item and show badge', () => {
    cy.addToCart(0);
    cy.cartShouldHave(1);
  });

  // This test does NOT depend on the previous one —
  // each test starts with a fresh login and empty cart
});


// ── Pattern 4: Conditional setup with cy.session() (Cypress 12+) ──

// cy.session() caches cookies/localStorage across tests for faster setup
Cypress.Commands.add('loginCached', (username: string, password: string) => {
  cy.session([username, password], () => {
    // This runs ONCE and caches the result
    cy.request('POST', '/api/auth/login', { username, password })
      .then((resp) => {
        window.localStorage.setItem('authToken', resp.body.token);
      });
  });
});

// Usage: cy.loginCached('user', 'pass') — first call is slow, subsequent are instant
Note: cy.session() (Cypress 12+) is a game-changer for test setup performance. It caches the browser state (cookies, localStorage, sessionStorage) after the first login and restores it instantly for subsequent tests that use the same credentials. A test suite with 50 tests that each call cy.loginCached('user', 'pass') performs the actual login only once — the other 49 tests restore the cached session in milliseconds. This can reduce suite execution time by 50% or more for authentication-heavy test suites.
Tip: If your application does not have a test API for seeding data, use cy.request() with the application’s real API endpoints to create the data your test needs. For example, POST to /api/orders to create a test order before testing the order history page. This is faster and more reliable than clicking through the UI to create the order. Most applications expose CRUD APIs that tests can leverage for setup even without a dedicated test seeding endpoint.
Warning: Do not use before() for setup that each test depends on. before() runs once for the entire describe block — if it creates a user account and Test 1 deletes that account, Tests 2-10 fail because the account no longer exists. Use beforeEach() for any setup that tests might modify. Reserve before() for truly one-time, read-only operations like loading large fixture files or checking environment prerequisites.

Common Mistakes

Mistake 1 — Tests depending on execution order

❌ Wrong: Test 1 creates a product. Test 2 assumes the product exists and adds it to cart. If Test 1 is skipped or fails, Test 2 fails too.

✅ Correct: Each test creates its own data in beforeEach() via API calls. Test 2 creates its own product, adds it to cart, and verifies — completely independent of Test 1.

Mistake 2 — Not cleaning up test data after tests

❌ Wrong: Tests create orders, users, and products but never clean them up — the test database grows until it affects performance or causes unique-constraint conflicts.

✅ Correct: afterEach() calls a cleanup API endpoint, or the beforeEach() resets the database to a known state before each test.

🧠 Test Yourself

What does cy.session() do and why is it valuable for test performance?