Day 6: ネットワークリクエストの制御
今日学ぶこと
- cy.intercept() の基本的な使い方
- HTTPメソッド別のインターセプト(GET, POST, PUT, DELETE)
- レスポンスのスタブ(固定データを返す)
- リクエストの待機(cy.wait() + alias)
- リクエストボディ・ヘッダーの検証
- エラーレスポンスのシミュレーション(404, 500)
- ネットワーク遅延のシミュレーション
- 実践:API連携画面のテスト
cy.intercept() とは
cy.intercept() は、ブラウザから送信されるHTTPリクエストを**傍受(インターセプト)**し、レスポンスを差し替えたり、リクエストの内容を検証したりするためのコマンドです。
flowchart LR
Browser["ブラウザ"] --> |"リクエスト"| Intercept["cy.intercept()\n傍受"]
Intercept --> |"そのまま通す"| Server["サーバー"]
Intercept --> |"スタブ応答"| Stub["固定レスポンス"]
Server --> |"レスポンス"| Browser
Stub --> |"レスポンス"| Browser
style Browser fill:#3b82f6,color:#fff
style Intercept fill:#f59e0b,color:#fff
style Server fill:#22c55e,color:#fff
style Stub fill:#8b5cf6,color:#fff
なぜインターセプトが必要か
| 課題 | cy.intercept() の解決策 |
|---|---|
| APIサーバーが不安定 | スタブで固定データを返す |
| テストデータの準備が大変 | 任意のレスポンスを設定できる |
| エラー系のテストが困難 | 404, 500などを簡単にシミュレート |
| ネットワーク遅延のテスト | delay オプションで遅延を再現 |
| APIコールの検証 | リクエスト内容を確認できる |
基本的な使い方
シンプルなインターセプト
// GETリクエストをインターセプト
cy.intercept('GET', '/api/users').as('getUsers')
// ページを開く
cy.visit('/users')
// リクエストの完了を待つ
cy.wait('@getUsers')
URLパターンのマッチング
// 完全一致
cy.intercept('GET', '/api/users')
// ワイルドカード
cy.intercept('GET', '/api/users/*') // /api/users/1, /api/users/abc
cy.intercept('GET', '/api/users/*/posts') // /api/users/1/posts
// 正規表現
cy.intercept('GET', /\/api\/users\/\d+$/) // /api/users/123
// クエリパラメータ付き(URLマッチャーオブジェクト)
cy.intercept({
method: 'GET',
url: '/api/users',
query: { page: '1', limit: '10' }
})
HTTPメソッド別のインターセプト
GET - データ取得
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: '山田太郎', email: 'yamada@example.com' },
{ id: 2, name: '鈴木花子', email: 'suzuki@example.com' }
]
}).as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')
cy.get('.user-card').should('have.length', 2)
cy.get('.user-card').first().should('contain', '山田太郎')
POST - データ作成
cy.intercept('POST', '/api/users', {
statusCode: 201,
body: { id: 3, name: '佐藤次郎', email: 'sato@example.com' }
}).as('createUser')
cy.get('#name').type('佐藤次郎')
cy.get('#email').type('sato@example.com')
cy.get('button[type="submit"]').click()
cy.wait('@createUser')
cy.get('.success-toast').should('contain', 'ユーザーを作成しました')
PUT - データ更新
cy.intercept('PUT', '/api/users/1', {
statusCode: 200,
body: { id: 1, name: '山田太郎(更新)', email: 'yamada-new@example.com' }
}).as('updateUser')
cy.get('#name').clear().type('山田太郎(更新)')
cy.get('button.save').click()
cy.wait('@updateUser')
DELETE - データ削除
cy.intercept('DELETE', '/api/users/1', {
statusCode: 204,
body: null
}).as('deleteUser')
cy.get('button.delete').click()
cy.get('.confirm-dialog button.yes').click()
cy.wait('@deleteUser')
cy.get('.user-card').should('have.length', 1)
flowchart TB
subgraph Methods["HTTPメソッド"]
GET["GET\nデータ取得"]
POST["POST\nデータ作成"]
PUT["PUT\nデータ更新"]
DELETE["DELETE\nデータ削除"]
end
subgraph StatusCodes["レスポンスコード"]
S200["200 OK"]
S201["201 Created"]
S204["204 No Content"]
end
GET --> S200
POST --> S201
PUT --> S200
DELETE --> S204
style GET fill:#3b82f6,color:#fff
style POST fill:#22c55e,color:#fff
style PUT fill:#f59e0b,color:#fff
style DELETE fill:#ef4444,color:#fff
style S200 fill:#8b5cf6,color:#fff
style S201 fill:#8b5cf6,color:#fff
style S204 fill:#8b5cf6,color:#fff
レスポンスのスタブ
fixture ファイルを使ったスタブ
テストデータを外部ファイルに管理すると、テストコードが見やすくなります。
// cypress/fixtures/users.json
// [
// { "id": 1, "name": "山田太郎", "email": "yamada@example.com" },
// { "id": 2, "name": "鈴木花子", "email": "suzuki@example.com" }
// ]
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')
動的レスポンス(routeHandler)
cy.intercept('GET', '/api/users/*', (req) => {
const userId = req.url.split('/').pop()
req.reply({
statusCode: 200,
body: {
id: Number(userId),
name: `ユーザー${userId}`,
email: `user${userId}@example.com`
}
})
}).as('getUser')
レスポンスヘッダーの設定
cy.intercept('GET', '/api/data', {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'X-Total-Count': '100',
'Cache-Control': 'no-cache'
},
body: { items: [] }
})
リクエストの待機と検証
cy.wait() + alias の基本
cy.intercept('POST', '/api/login').as('loginRequest')
cy.get('#email').type('user@example.com')
cy.get('#password').type('password123')
cy.get('button[type="submit"]').click()
// リクエストの完了を待機
cy.wait('@loginRequest').then((interception) => {
// リクエストの内容を検証
expect(interception.request.body).to.deep.equal({
email: 'user@example.com',
password: 'password123'
})
// レスポンスの内容を検証
expect(interception.response.statusCode).to.equal(200)
})
リクエストヘッダーの検証
cy.intercept('GET', '/api/protected').as('protectedRequest')
cy.visit('/protected-page')
cy.wait('@protectedRequest').then((interception) => {
expect(interception.request.headers).to.have.property('authorization')
expect(interception.request.headers.authorization).to.include('Bearer')
})
複数リクエストの待機
cy.intercept('GET', '/api/users').as('getUsers')
cy.intercept('GET', '/api/posts').as('getPosts')
cy.intercept('GET', '/api/comments').as('getComments')
cy.visit('/dashboard')
// 複数のリクエストを順番に待機
cy.wait(['@getUsers', '@getPosts', '@getComments'])
// すべてのデータが表示されていることを確認
cy.get('.users-section').should('be.visible')
cy.get('.posts-section').should('be.visible')
cy.get('.comments-section').should('be.visible')
エラーレスポンスのシミュレーション
HTTPエラーコード
// 404 Not Found
cy.intercept('GET', '/api/users/999', {
statusCode: 404,
body: { error: 'User not found' }
}).as('getUserNotFound')
cy.visit('/users/999')
cy.wait('@getUserNotFound')
cy.get('.error-message').should('contain', 'ユーザーが見つかりません')
// 500 Internal Server Error
cy.intercept('POST', '/api/users', {
statusCode: 500,
body: { error: 'Internal server error' }
}).as('serverError')
cy.get('form').submit()
cy.wait('@serverError')
cy.get('.error-toast').should('contain', 'サーバーエラーが発生しました')
// 401 Unauthorized
cy.intercept('GET', '/api/protected', {
statusCode: 401,
body: { error: 'Unauthorized' }
}).as('unauthorized')
cy.visit('/protected-page')
cy.wait('@unauthorized')
cy.url().should('include', '/login')
よく使うHTTPステータスコード
| コード | 名前 | テスト用途 |
|---|---|---|
| 200 | OK | 正常レスポンス |
| 201 | Created | 作成成功 |
| 204 | No Content | 削除成功 |
| 400 | Bad Request | バリデーションエラー |
| 401 | Unauthorized | 認証エラー |
| 403 | Forbidden | 権限エラー |
| 404 | Not Found | リソース不在 |
| 409 | Conflict | 競合エラー |
| 422 | Unprocessable Entity | バリデーションエラー |
| 500 | Internal Server Error | サーバーエラー |
ネットワーク遅延のシミュレーション
delay オプション
// 3秒の遅延を追加
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [{ id: 1, name: '山田太郎' }],
delay: 3000 // 3000ms = 3秒
}).as('slowRequest')
cy.visit('/users')
// ローディング表示の確認
cy.get('.loading-spinner').should('be.visible')
// データ表示後にローディングが消えることを確認
cy.wait('@slowRequest')
cy.get('.loading-spinner').should('not.exist')
cy.get('.user-card').should('be.visible')
throttle(帯域制限)
cy.intercept('GET', '/api/large-data', {
statusCode: 200,
body: { data: '大量のデータ...' },
throttleKbps: 50 // 50KB/秒に制限
}).as('throttledRequest')
sequenceDiagram
participant Browser as ブラウザ
participant Intercept as cy.intercept()
participant Server as サーバー
Browser->>Intercept: GETリクエスト
Note over Intercept: delay: 3000ms
Intercept-->>Browser: ローディング表示中...
Intercept->>Browser: レスポンス(3秒後)
Note over Browser: データ表示
実践:API連携画面のテスト
TODOアプリのCRUDテスト
describe('TODOアプリ API連携テスト', () => {
const todos = [
{ id: 1, title: '買い物', completed: false },
{ id: 2, title: '掃除', completed: true },
{ id: 3, title: '料理', completed: false }
]
beforeEach(() => {
// TODO一覧取得のスタブ
cy.intercept('GET', '/api/todos', {
statusCode: 200,
body: todos
}).as('getTodos')
cy.visit('/todos')
cy.wait('@getTodos')
})
it('TODO一覧が表示される', () => {
cy.get('.todo-item').should('have.length', 3)
cy.get('.todo-item').first().should('contain', '買い物')
})
it('新しいTODOを追加できる', () => {
cy.intercept('POST', '/api/todos', {
statusCode: 201,
body: { id: 4, title: '勉強', completed: false }
}).as('createTodo')
cy.get('#new-todo').type('勉強')
cy.get('button.add').click()
cy.wait('@createTodo').then((interception) => {
expect(interception.request.body).to.deep.equal({
title: '勉強',
completed: false
})
})
})
it('TODOの完了状態を切り替えられる', () => {
cy.intercept('PUT', '/api/todos/1', {
statusCode: 200,
body: { id: 1, title: '買い物', completed: true }
}).as('toggleTodo')
cy.get('.todo-item').first().find('input[type="checkbox"]').check()
cy.wait('@toggleTodo').then((interception) => {
expect(interception.request.body.completed).to.equal(true)
})
})
it('TODOを削除できる', () => {
cy.intercept('DELETE', '/api/todos/1', {
statusCode: 204
}).as('deleteTodo')
cy.get('.todo-item').first().find('button.delete').click()
cy.wait('@deleteTodo')
})
it('サーバーエラー時にエラーメッセージが表示される', () => {
cy.intercept('POST', '/api/todos', {
statusCode: 500,
body: { error: 'サーバーエラー' }
}).as('createTodoError')
cy.get('#new-todo').type('テスト')
cy.get('button.add').click()
cy.wait('@createTodoError')
cy.get('.error-message').should('be.visible')
})
it('ネットワーク遅延時にローディングが表示される', () => {
cy.intercept('GET', '/api/todos', {
statusCode: 200,
body: todos,
delay: 2000
}).as('slowGetTodos')
cy.visit('/todos')
cy.get('.loading').should('be.visible')
cy.wait('@slowGetTodos')
cy.get('.loading').should('not.exist')
})
})
リクエストの変更(req.continue)
// リクエストを変更してからサーバーに送る
cy.intercept('GET', '/api/users', (req) => {
// ヘッダーを追加
req.headers['X-Custom-Header'] = 'test-value'
// サーバーに送信
req.continue((res) => {
// レスポンスを変更
res.body.push({ id: 99, name: 'テストユーザー' })
res.send()
})
})
まとめ
| カテゴリ | コマンド/オプション | 用途 |
|---|---|---|
| 基本 | cy.intercept(method, url) |
リクエストを傍受 |
| エイリアス | .as('alias') |
待機・参照用の名前を付ける |
| 待機 | cy.wait('@alias') |
リクエスト完了まで待つ |
| スタブ | { statusCode, body } |
固定レスポンスを返す |
| fixture | { fixture: 'file.json' } |
外部ファイルからレスポンスを返す |
| 動的応答 | req.reply() |
動的にレスポンスを生成 |
| エラー | { statusCode: 500 } |
エラーレスポンスをシミュレート |
| 遅延 | { delay: 3000 } |
ネットワーク遅延をシミュレート |
| 帯域制限 | { throttleKbps: 50 } |
帯域を制限 |
| リクエスト検証 | interception.request.body |
リクエスト内容を検証 |
重要ポイント
- テストの独立性を保つ - cy.intercept() でAPIをスタブ化し、バックエンドに依存しないテストを書く
- alias + wait パターン - リクエストにエイリアスを付けて待機する、Cypressの基本パターンを身につける
- fixture ファイルを活用する - テストデータはfixtureとして管理すると、再利用性が高まる
- エラー系を必ずテストする - 正常系だけでなく、404、500などのエラーハンドリングもテストする
- ローディングUIをテストする - delay オプションで遅延を再現し、ローディング状態の表示を確認する
練習問題
基本
cy.intercept()を使ってGETリクエストをスタブ化し、固定データを返してくださいcy.wait()を使ってリクエストの完了を待機し、レスポンスのステータスコードを検証してください- fixture ファイルを作成し、それをレスポンスとして返すインターセプトを設定してください
応用
- POST、PUT、DELETEリクエストをそれぞれインターセプトし、リクエストボディを検証してください
- 404と500のエラーレスポンスをシミュレートし、画面のエラー表示をテストしてください
- ネットワーク遅延をシミュレートし、ローディング表示の出現と消失をテストしてください
チャレンジ
- CRUDすべての操作をカバーするAPI連携テストスイートを作成してください
- 同じエンドポイントに対して、リクエストごとに異なるレスポンスを返すテストを作成してください(1回目は成功、2回目はエラーなど)
参考リンク
次回予告
Day 7では、カスタムコマンドとユーティリティを学びます。繰り返し使う操作をカスタムコマンドとして定義し、テストコードの再利用性と可読性を向上させる方法を習得しましょう。