Day 8: フィクスチャとデータ駆動テスト
今日学ぶこと
- cy.fixture() でテストデータを読み込む方法
- JSONフィクスチャファイルの作成と管理
- フィクスチャと cy.intercept() の組み合わせ
- エイリアス(.as())の活用
- データ駆動テストのパターン
- 複数データセットでのテスト実行
- テストデータの管理戦略
フィクスチャとは
フィクスチャ(Fixture)とは、テストで使用する固定のデータセットです。テストデータを外部ファイルに分離することで、テストコードをシンプルに保てます。
flowchart LR
subgraph Fixtures["cypress/fixtures/"]
F1["users.json"]
F2["products.json"]
F3["api-response.json"]
end
subgraph Tests["テストファイル"]
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
| 手法 | テストデータの場所 | メリット | デメリット |
|---|---|---|---|
| ハードコード | テストファイル内 | 簡単 | 重複、メンテナンス困難 |
| フィクスチャ | 外部JSONファイル | 整理しやすい、再利用可能 | ファイル管理が必要 |
| API/DB生成 | テスト実行時に生成 | 常に最新 | セットアップが複雑 |
cy.fixture() の基本
フィクスチャファイルの作成
フィクスチャは cypress/fixtures/ ディレクトリに配置します。
// cypress/fixtures/user.json
{
"id": 1,
"name": "Taro Yamada",
"email": "taro@example.com",
"role": "admin",
"isActive": true
}
// cypress/fixtures/users.json
[
{
"id": 1,
"name": "Taro Yamada",
"email": "taro@example.com",
"role": "admin"
},
{
"id": 2,
"name": "Hanako Suzuki",
"email": "hanako@example.com",
"role": "user"
},
{
"id": 3,
"name": "Jiro Tanaka",
"email": "jiro@example.com",
"role": "editor"
}
]
フィクスチャの読み込み
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)
})
})
})
サブディレクトリの活用
cypress/fixtures/
├── auth/
│ ├── login-success.json
│ └── login-failure.json
├── users/
│ ├── admin.json
│ ├── regular.json
│ └── list.json
└── products/
├── single.json
└── catalog.json
// サブディレクトリのフィクスチャを読み込む
cy.fixture('auth/login-success.json').then((data) => {
// ...
})
エイリアス(.as())の活用
cy.fixture() と .as() を組み合わせることで、テスト全体でフィクスチャデータを参照できます。
beforeEach でエイリアスを設定
describe('User Management', () => {
beforeEach(() => {
cy.fixture('users.json').as('users')
cy.fixture('user.json').as('singleUser')
})
it('should display user list', function () {
// 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)
})
})
注意: エイリアスを
thisでアクセスする場合、アロー関数(() =>)ではなく通常の関数(function())を使う必要があります。
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)
})
})
})
| アクセス方法 | 構文 | 関数の種類 |
|---|---|---|
| this | this.aliasName |
function() が必要 |
| cy.get() | cy.get('@aliasName') |
アロー関数でもOK |
フィクスチャと cy.intercept() の組み合わせ
フィクスチャの最も強力な使い方は、APIレスポンスのモック(スタブ)です。
APIレスポンスをモック
// cypress/fixtures/api/users-list.json
{
"data": [
{ "id": 1, "name": "Taro", "email": "taro@example.com" },
{ "id": 2, "name": "Hanako", "email": "hanako@example.com" }
],
"total": 2,
"page": 1
}
describe('Users Page', () => {
it('should display users from API', () => {
// APIレスポンスをフィクスチャでモック
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', 'Taro')
})
})
ステータスコードやヘッダーも制御
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["テストコード"] -->|"cy.intercept()"| INTERCEPT["インターセプト設定"]
INTERCEPT -->|"fixture指定"| FIXTURE["fixtures/api/users.json"]
APP["アプリケーション"] -->|"GET /api/users"| INTERCEPT
INTERCEPT -->|"モックレスポンス"| 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
条件に応じたレスポンスの切り替え
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('Taro')
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')
})
})
データ駆動テスト
同じテストロジックを異なるデータセットで繰り返し実行するパターンです。
配列を使ったデータ駆動テスト
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)
})
})
})
フィクスチャを使ったデータ駆動テスト
// 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)
}
})
})
})
フォームバリデーションのデータ駆動テスト
// 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["データソース"]
JSON["fixtures/\ntest-cases.json"]
ARRAY["テストコード内\n配列定義"]
end
subgraph Execution["テスト実行"]
LOOP["forEach で\nデータを反復"]
T1["テストケース 1"]
T2["テストケース 2"]
T3["テストケース N"]
LOOP --> T1
LOOP --> T2
LOOP --> T3
end
JSON -->|"cy.fixture()"| LOOP
ARRAY -->|"直接参照"| LOOP
style DataSource fill:#3b82f6,color:#fff
style Execution fill:#22c55e,color:#fff
実践: ユーザー一覧画面のテスト
フィクスチャとデータ駆動テストを組み合わせた実践例です。
フィクスチャの準備
// cypress/fixtures/users/list-page1.json
{
"users": [
{ "id": 1, "name": "Taro Yamada", "email": "taro@example.com", "status": "active" },
{ "id": 2, "name": "Hanako Suzuki", "email": "hanako@example.com", "status": "active" },
{ "id": 3, "name": "Jiro Tanaka", "email": "jiro@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
}
}
テストコード
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')
})
})
})
})
テストデータの管理戦略
プロジェクトが大きくなるにつれ、テストデータの管理が重要になります。
flowchart TB
subgraph Strategy["テストデータ管理戦略"]
direction TB
S1["静的フィクスチャ\n(JSON ファイル)"]
S2["動的生成\n(ファクトリ関数)"]
S3["APIシード\n(テスト前にDBセットアップ)"]
end
S1 -->|"モックレスポンス\nUI表示テスト"| USE1["単純なデータ"]
S2 -->|"ランダムデータ\nエッジケース"| USE2["複雑なデータ"]
S3 -->|"E2Eテスト\n実データが必要"| USE3["統合テスト"]
style S1 fill:#3b82f6,color:#fff
style S2 fill:#8b5cf6,color:#fff
style S3 fill:#f59e0b,color:#fff
ファクトリパターン
// 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,
},
}
}
// テストでの使い方
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')
})
})
| パターン | 使いどころ | 例 |
|---|---|---|
| 静的フィクスチャ | APIモック、固定データ | users.json |
| ファクトリ関数 | 動的データ生成、カスタマイズ | createUserData() |
| Faker/Chance | ランダムなリアルデータ | faker.person.fullName() |
| DBシード | 統合テスト、E2E | cy.task('db:seed') |
まとめ
| 概念 | 説明 |
|---|---|
| cy.fixture() | fixtures/ ディレクトリからテストデータを読み込む |
| .as() | フィクスチャにエイリアスを付けて再利用 |
| cy.intercept() + fixture | APIレスポンスをフィクスチャでモック |
| データ駆動テスト | 配列やフィクスチャでテストケースを動的に生成 |
| ファクトリパターン | 関数でテストデータを動的に生成 |
| テストデータ管理 | 静的/動的/DB シードを用途に応じて使い分け |
重要ポイント
- テストデータはテストコードから分離し、フィクスチャとして管理する
cy.intercept()と組み合わせることで、バックエンドに依存しない安定したテストが書けるforEachでテストケースを回すデータ駆動テストは、網羅性と保守性を両立する- プロジェクトの規模に応じて、静的フィクスチャとファクトリパターンを使い分ける
練習問題
問題1: 基本
商品データのフィクスチャファイル(products.json)を作成し、商品一覧ページのテストで cy.fixture() を使って読み込んでください。
問題2: 応用
cy.intercept() を使って以下のシナリオをテストしてください。
- 正常時: 商品一覧APIが200を返す
- エラー時: 商品一覧APIが500を返す
- 空データ: 商品が0件の場合
チャレンジ問題
ファクトリパターンを使って、任意の件数・属性の商品データを生成する createProductData() 関数を作成してください。その関数を使い、「10件表示」「50件表示」「100件表示」のページネーションをデータ駆動テストで検証しましょう。
参考リンク
次回予告: Day 9では「ビジュアルテストとアクセシビリティ」について学びます。スクリーンショットの比較やa11yチェックを自動化する方法を習得しましょう。