10日で覚えるCypressDay 8: フィクスチャとデータ駆動テスト
books.chapter 810日で覚えるCypress

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 シードを用途に応じて使い分け

重要ポイント

  1. テストデータはテストコードから分離し、フィクスチャとして管理する
  2. cy.intercept() と組み合わせることで、バックエンドに依存しない安定したテストが書ける
  3. forEach でテストケースを回すデータ駆動テストは、網羅性と保守性を両立する
  4. プロジェクトの規模に応じて、静的フィクスチャとファクトリパターンを使い分ける

練習問題

問題1: 基本

商品データのフィクスチャファイル(products.json)を作成し、商品一覧ページのテストで cy.fixture() を使って読み込んでください。

問題2: 応用

cy.intercept() を使って以下のシナリオをテストしてください。

  • 正常時: 商品一覧APIが200を返す
  • エラー時: 商品一覧APIが500を返す
  • 空データ: 商品が0件の場合

チャレンジ問題

ファクトリパターンを使って、任意の件数・属性の商品データを生成する createProductData() 関数を作成してください。その関数を使い、「10件表示」「50件表示」「100件表示」のページネーションをデータ駆動テストで検証しましょう。


参考リンク


次回予告: Day 9では「ビジュアルテストとアクセシビリティ」について学びます。スクリーンショットの比較やa11yチェックを自動化する方法を習得しましょう。