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
- Separate test data from test code by managing it as fixtures
- Combine with
cy.intercept()for stable tests that don't depend on the backend - Data-driven testing with
forEachbalances coverage and maintainability - 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.