Learn Cypress in 10 DaysDay 8: Fixtures and Data-Driven Testing
Chapter 8Learn Cypress in 10 Days

Day 8: Fixtures and Data-Driven Testing

What You'll Learn Today

  • How to load test data with cy.fixture()
  • Creating and managing JSON fixture files
  • Combining fixtures with cy.intercept()
  • Using aliases with .as()
  • Data-driven testing patterns
  • Running tests with multiple data sets
  • Test data management strategies

What Are Fixtures?

A fixture is a fixed data set used in tests. By separating test data into external files, you can keep your test code clean and simple.

flowchart LR
    subgraph Fixtures["cypress/fixtures/"]
        F1["users.json"]
        F2["products.json"]
        F3["api-response.json"]
    end
    subgraph Tests["Test Files"]
        T1["user.spec.js"]
        T2["product.spec.js"]
    end
    F1 -->|"cy.fixture()"| T1
    F2 -->|"cy.fixture()"| T2
    F3 -->|"cy.intercept()"| T1
    F3 -->|"cy.intercept()"| T2
    style Fixtures fill:#3b82f6,color:#fff
    style Tests fill:#22c55e,color:#fff
Approach Data Location Pros Cons
Hardcoded Inside test file Simple Duplication, hard to maintain
Fixtures External JSON files Organized, reusable Requires file management
API/DB generated Generated at runtime Always fresh Complex setup

Basics of cy.fixture()

Creating Fixture Files

Fixtures are placed in the cypress/fixtures/ directory.

// cypress/fixtures/user.json
{
  "id": 1,
  "name": "John Smith",
  "email": "john@example.com",
  "role": "admin",
  "isActive": true
}
// cypress/fixtures/users.json
[
  {
    "id": 1,
    "name": "John Smith",
    "email": "john@example.com",
    "role": "admin"
  },
  {
    "id": 2,
    "name": "Jane Doe",
    "email": "jane@example.com",
    "role": "user"
  },
  {
    "id": 3,
    "name": "Bob Wilson",
    "email": "bob@example.com",
    "role": "editor"
  }
]

Loading Fixtures

describe('User Profile', () => {
  it('should display user information', () => {
    cy.fixture('user.json').then((user) => {
      cy.visit(`/users/${user.id}`)
      cy.get('[data-testid="user-name"]').should('contain', user.name)
      cy.get('[data-testid="user-email"]').should('contain', user.email)
    })
  })
})

Using Subdirectories

cypress/fixtures/
β”œβ”€β”€ auth/
β”‚   β”œβ”€β”€ login-success.json
β”‚   └── login-failure.json
β”œβ”€β”€ users/
β”‚   β”œβ”€β”€ admin.json
β”‚   β”œβ”€β”€ regular.json
β”‚   └── list.json
└── products/
    β”œβ”€β”€ single.json
    └── catalog.json
// Load a fixture from a subdirectory
cy.fixture('auth/login-success.json').then((data) => {
  // ...
})

Using Aliases with .as()

By combining cy.fixture() with .as(), you can reference fixture data throughout your tests.

Setting Aliases in beforeEach

describe('User Management', () => {
  beforeEach(() => {
    cy.fixture('users.json').as('users')
    cy.fixture('user.json').as('singleUser')
  })

  it('should display user list', function () {
    // Access alias via this.users
    cy.visit('/users')
    this.users.forEach((user) => {
      cy.contains(user.name).should('be.visible')
    })
  })

  it('should show user details', function () {
    cy.visit(`/users/${this.singleUser.id}`)
    cy.get('[data-testid="user-name"]').should('contain', this.singleUser.name)
  })
})

Note: When accessing aliases via this, you must use regular functions (function()) instead of arrow functions (() =>).

Referencing with cy.get('@alias')

describe('User Profile', () => {
  beforeEach(() => {
    cy.fixture('user.json').as('userData')
  })

  it('should display user data', () => {
    cy.get('@userData').then((user) => {
      cy.visit(`/users/${user.id}`)
      cy.get('[data-testid="user-name"]').should('contain', user.name)
    })
  })
})
Access Method Syntax Function Type
this this.aliasName Requires function()
cy.get() cy.get('@aliasName') Arrow functions OK

Combining Fixtures with cy.intercept()

The most powerful use of fixtures is mocking (stubbing) API responses.

Mocking API Responses

// cypress/fixtures/api/users-list.json
{
  "data": [
    { "id": 1, "name": "John", "email": "john@example.com" },
    { "id": 2, "name": "Jane", "email": "jane@example.com" }
  ],
  "total": 2,
  "page": 1
}
describe('Users Page', () => {
  it('should display users from API', () => {
    // Mock API response with a fixture
    cy.intercept('GET', '/api/users', { fixture: 'api/users-list.json' }).as('getUsers')

    cy.visit('/users')
    cy.wait('@getUsers')

    cy.get('[data-testid="user-row"]').should('have.length', 2)
    cy.get('[data-testid="user-row"]').first().should('contain', 'John')
  })
})

Controlling Status Codes and Headers

it('should handle API errors', () => {
  cy.intercept('GET', '/api/users', {
    statusCode: 500,
    fixture: 'api/error-response.json',
    headers: {
      'Content-Type': 'application/json',
    },
  }).as('getUsersError')

  cy.visit('/users')
  cy.wait('@getUsersError')
  cy.get('[data-testid="error-message"]').should('contain', 'Server Error')
})
flowchart TB
    TEST["Test Code"] -->|"cy.intercept()"| INTERCEPT["Intercept Setup"]
    INTERCEPT -->|"fixture option"| FIXTURE["fixtures/api/users.json"]
    APP["Application"] -->|"GET /api/users"| INTERCEPT
    INTERCEPT -->|"Mock response"| APP
    style TEST fill:#3b82f6,color:#fff
    style INTERCEPT fill:#8b5cf6,color:#fff
    style FIXTURE fill:#f59e0b,color:#fff
    style APP fill:#22c55e,color:#fff

Switching Responses Based on Conditions

describe('User Search', () => {
  it('should show results for valid search', () => {
    cy.intercept('GET', '/api/users?q=*', {
      fixture: 'api/search-results.json',
    })

    cy.visit('/users')
    cy.get('#search').type('John')
    cy.get('[data-testid="search-result"]').should('have.length.greaterThan', 0)
  })

  it('should show empty state for no results', () => {
    cy.intercept('GET', '/api/users?q=*', {
      body: { data: [], total: 0 },
    })

    cy.visit('/users')
    cy.get('#search').type('NonExistentUser')
    cy.get('[data-testid="empty-state"]').should('be.visible')
  })
})

Data-Driven Testing

Data-driven testing is a pattern where the same test logic runs against different data sets.

Array-Based Data-Driven Tests

const loginCases = [
  { email: 'admin@example.com', password: 'admin123', expectedRole: 'Admin' },
  { email: 'user@example.com', password: 'user123', expectedRole: 'User' },
  { email: 'editor@example.com', password: 'editor123', expectedRole: 'Editor' },
]

describe('Login with different roles', () => {
  loginCases.forEach((testCase) => {
    it(`should login as ${testCase.expectedRole}`, () => {
      cy.visit('/login')
      cy.get('#email').type(testCase.email)
      cy.get('#password').type(testCase.password)
      cy.get('button[type="submit"]').click()
      cy.get('[data-testid="user-role"]').should('contain', testCase.expectedRole)
    })
  })
})

Fixture-Based Data-Driven Tests

// cypress/fixtures/test-cases/login-cases.json
[
  {
    "description": "valid admin login",
    "email": "admin@example.com",
    "password": "admin123",
    "shouldSucceed": true,
    "expectedMessage": "Welcome, Admin"
  },
  {
    "description": "valid user login",
    "email": "user@example.com",
    "password": "user123",
    "shouldSucceed": true,
    "expectedMessage": "Welcome, User"
  },
  {
    "description": "invalid password",
    "email": "admin@example.com",
    "password": "wrong",
    "shouldSucceed": false,
    "expectedMessage": "Invalid credentials"
  }
]
describe('Login scenarios', () => {
  beforeEach(() => {
    cy.fixture('test-cases/login-cases.json').as('loginCases')
  })

  it('should handle all login scenarios', function () {
    this.loginCases.forEach((testCase) => {
      cy.visit('/login')
      cy.get('#email').type(testCase.email)
      cy.get('#password').type(testCase.password)
      cy.get('button[type="submit"]').click()

      if (testCase.shouldSucceed) {
        cy.get('[data-testid="welcome"]').should('contain', testCase.expectedMessage)
        cy.get('[data-testid="logout"]').click()
      } else {
        cy.get('[data-testid="error"]').should('contain', testCase.expectedMessage)
      }
    })
  })
})

Data-Driven Form Validation Tests

// cypress/fixtures/test-cases/validation-cases.json
[
  {
    "field": "email",
    "value": "",
    "error": "Email is required"
  },
  {
    "field": "email",
    "value": "not-an-email",
    "error": "Invalid email format"
  },
  {
    "field": "password",
    "value": "",
    "error": "Password is required"
  },
  {
    "field": "password",
    "value": "123",
    "error": "Password must be at least 8 characters"
  }
]
describe('Form Validation', () => {
  beforeEach(() => {
    cy.visit('/register')
  })

  // dynamically create tests from fixture
  before(() => {
    cy.fixture('test-cases/validation-cases.json').as('validationCases')
  })

  it('should show validation errors', function () {
    this.validationCases.forEach((testCase) => {
      cy.get(`#${testCase.field}`).clear()
      if (testCase.value) {
        cy.get(`#${testCase.field}`).type(testCase.value)
      }
      cy.get(`#${testCase.field}`).blur()
      cy.get(`[data-testid="${testCase.field}-error"]`).should('contain', testCase.error)
    })
  })
})
flowchart TB
    subgraph DataSource["Data Source"]
        JSON["fixtures/\ntest-cases.json"]
        ARRAY["In-code\narray definition"]
    end
    subgraph Execution["Test Execution"]
        LOOP["forEach iterates\nover data"]
        T1["Test Case 1"]
        T2["Test Case 2"]
        T3["Test Case N"]
        LOOP --> T1
        LOOP --> T2
        LOOP --> T3
    end
    JSON -->|"cy.fixture()"| LOOP
    ARRAY -->|"Direct reference"| LOOP
    style DataSource fill:#3b82f6,color:#fff
    style Execution fill:#22c55e,color:#fff

Practical Example: Testing a User List Page

Here's a practical example combining fixtures with data-driven testing.

Preparing Fixtures

// cypress/fixtures/users/list-page1.json
{
  "users": [
    { "id": 1, "name": "John Smith", "email": "john@example.com", "status": "active" },
    { "id": 2, "name": "Jane Doe", "email": "jane@example.com", "status": "active" },
    { "id": 3, "name": "Bob Wilson", "email": "bob@example.com", "status": "inactive" }
  ],
  "pagination": {
    "currentPage": 1,
    "totalPages": 3,
    "totalItems": 9,
    "itemsPerPage": 3
  }
}
// cypress/fixtures/users/empty.json
{
  "users": [],
  "pagination": {
    "currentPage": 1,
    "totalPages": 0,
    "totalItems": 0,
    "itemsPerPage": 3
  }
}

Test Code

describe('Users List Page', () => {
  describe('with data', () => {
    beforeEach(() => {
      cy.intercept('GET', '/api/users*', {
        fixture: 'users/list-page1.json',
      }).as('getUsers')

      cy.visit('/users')
      cy.wait('@getUsers')
    })

    it('should display the correct number of users', () => {
      cy.get('[data-testid="user-row"]').should('have.length', 3)
    })

    it('should display user information correctly', () => {
      cy.fixture('users/list-page1.json').then((data) => {
        data.users.forEach((user, index) => {
          cy.get('[data-testid="user-row"]')
            .eq(index)
            .within(() => {
              cy.get('[data-testid="user-name"]').should('contain', user.name)
              cy.get('[data-testid="user-email"]').should('contain', user.email)
              cy.get('[data-testid="user-status"]').should('contain', user.status)
            })
        })
      })
    })

    it('should show pagination info', () => {
      cy.get('[data-testid="pagination-info"]').should('contain', 'Page 1 of 3')
      cy.get('[data-testid="total-items"]').should('contain', '9 users')
    })
  })

  describe('empty state', () => {
    beforeEach(() => {
      cy.intercept('GET', '/api/users*', {
        fixture: 'users/empty.json',
      }).as('getUsers')

      cy.visit('/users')
      cy.wait('@getUsers')
    })

    it('should show empty state message', () => {
      cy.get('[data-testid="user-row"]').should('not.exist')
      cy.get('[data-testid="empty-state"]').should('be.visible')
      cy.get('[data-testid="empty-state"]').should('contain', 'No users found')
    })
  })

  describe('status filtering', () => {
    const statuses = ['active', 'inactive', 'all']

    statuses.forEach((status) => {
      it(`should filter by ${status} status`, () => {
        cy.intercept('GET', `/api/users?status=${status}`, {
          fixture: `users/filter-${status}.json`,
        }).as('filteredUsers')

        cy.visit('/users')
        cy.get('[data-testid="status-filter"]').select(status)
        cy.wait('@filteredUsers')
        cy.get('[data-testid="user-row"]').should('exist')
      })
    })
  })
})

Test Data Management Strategies

As your project grows, managing test data becomes increasingly important.

flowchart TB
    subgraph Strategy["Test Data Management Strategies"]
        direction TB
        S1["Static Fixtures\n(JSON files)"]
        S2["Dynamic Generation\n(Factory functions)"]
        S3["API Seeding\n(DB setup before tests)"]
    end
    S1 -->|"Mock responses\nUI display tests"| USE1["Simple data"]
    S2 -->|"Random data\nEdge cases"| USE2["Complex data"]
    S3 -->|"E2E tests\nReal data needed"| USE3["Integration tests"]
    style S1 fill:#3b82f6,color:#fff
    style S2 fill:#8b5cf6,color:#fff
    style S3 fill:#f59e0b,color:#fff

Factory Pattern

// cypress/support/factories/user.js
let idCounter = 0

export function createUserData(overrides = {}) {
  idCounter += 1
  return {
    id: idCounter,
    name: `Test User ${idCounter}`,
    email: `user${idCounter}@example.com`,
    role: 'user',
    status: 'active',
    createdAt: new Date().toISOString(),
    ...overrides,
  }
}

export function createUsersListResponse(count = 5, overrides = {}) {
  const users = Array.from({ length: count }, (_, i) =>
    createUserData({ id: i + 1, ...overrides })
  )
  return {
    users,
    pagination: {
      currentPage: 1,
      totalPages: Math.ceil(count / 10),
      totalItems: count,
      itemsPerPage: 10,
    },
  }
}
// Usage in tests
import { createUserData, createUsersListResponse } from '../support/factories/user'

describe('Users Page', () => {
  it('should display 20 users', () => {
    const response = createUsersListResponse(20)
    cy.intercept('GET', '/api/users', { body: response }).as('getUsers')
    cy.visit('/users')
    cy.wait('@getUsers')
    cy.get('[data-testid="total-items"]').should('contain', '20 users')
  })

  it('should highlight admin users', () => {
    const admin = createUserData({ role: 'admin', name: 'Admin User' })
    cy.intercept('GET', `/api/users/${admin.id}`, { body: admin })
    cy.visit(`/users/${admin.id}`)
    cy.get('[data-testid="admin-badge"]').should('be.visible')
  })
})
Pattern When to Use Example
Static fixtures API mocks, fixed data users.json
Factory functions Dynamic data, customization createUserData()
Faker/Chance Random realistic data faker.person.fullName()
DB seeding Integration tests, E2E cy.task('db:seed')

Summary

Concept Description
cy.fixture() Loads test data from the fixtures/ directory
.as() Assigns an alias to a fixture for reuse
cy.intercept() + fixture Mocks API responses with fixture data
Data-driven testing Dynamically generates test cases from arrays or fixtures
Factory pattern Generates test data dynamically with functions
Test data management Choose between static, dynamic, or DB seeding based on needs

Key Takeaways

  1. Separate test data from test code by managing it as fixtures
  2. Combine with cy.intercept() for stable tests that don't depend on the backend
  3. Data-driven testing with forEach balances coverage and maintainability
  4. Choose between static fixtures and factory patterns based on your project's scale

Exercises

Exercise 1: Basics

Create a product data fixture file (products.json) and use cy.fixture() to load it in a product listing page test.

Exercise 2: Intermediate

Use cy.intercept() to test the following scenarios:

  • Success: Product list API returns 200
  • Error: Product list API returns 500
  • Empty data: Zero products returned

Challenge

Create a createProductData() factory function that generates product data with any count and attributes. Use that function to write data-driven tests verifying pagination for "10 items", "50 items", and "100 items" views.


References


Next up: In Day 9, we'll cover visual testing and accessibility. You'll learn how to automate screenshot comparisons and a11y checks.