Cypress intercept() 代替手段:ネットワークテストの選択肢
Cypressにおける「intercept」とは何か?
Cypressのcy.intercept()
コマンドは、ウェブアプリケーションが発行するネットワークリクエスト(HTTP/HTTPSリクエスト)を捕捉(傍受)し、操作するための強力な機能です。これにより、フロントエンドのテストにおいて、以下のような様々なシナリオを実現できます。
- スタブ(Stubbing): 実際のリクエストをサーバーに送信する代わりに、Cypress側で定義した偽のレスポンスを返す。これにより、サーバーの状態に依存しないテストが可能になります。
- モック(Mocking): スタブと似ていますが、通常はより複雑なデータ構造や振る舞いを模倣する場合に用いられます。
- スパイ(Spying): 実際のリクエストはサーバーに送信させつつ、そのリクエストの内容(URL、メソッド、ヘッダー、ボディなど)や、サーバーからのレスポンスの内容を監視・検証する。
- 変更(Modifying): リクエストがサーバーに到達する前、またはレスポンスがクライアントに到達する前に、それらの内容を動的に変更する。
なぜ「intercept」が必要なのか?
フロントエンドのテスト、特にE2E(End-to-End)テストやコンポーネントテストにおいて、ネットワークリクエストの制御は非常に重要です。その主な理由は以下の通りです。
- 開発環境の分離: 開発者がAPIの準備を待つことなく、フロントエンドの開発とテストを進めることができます。
- 特定のシナリオのテスト:
- APIからのエラーレスポンス(404, 500など)をシミュレートし、エラーハンドリングが適切に動作するかをテストする。
- 特定のデータが返された場合のUIの表示をテストする。
- 大量のデータが返された場合のパフォーマンスをテストする(この場合はモックデータを使用)。
- ネットワークの遅延をシミュレートし、ローディングスピナーなどが適切に表示されるかテストする。
- テストの速度: 実際のリクエストを待つ必要がなくなるため、テスト実行時間を大幅に短縮できます。
- テストの安定性: サーバーのダウンタイム、ネットワークの遅延、APIの変更など、外部要因にテストが左右されるのを防ぎます。
cy.intercept()
の基本的な構文は以下の通りです。
cy.intercept(method, url, handler)
handler
: リクエストを処理する方法を指定するオプション。オブジェクトまたは関数を指定します。url
: リクエストのURL。文字列、正規表現、またはGlobパターン(例:'/api/*'
)を使用できます。method
: HTTPメソッド(例:'GET'
,'POST'
,'PUT'
,'DELETE'
)。ワイルドカード*
も使用できます。
例1: GETリクエストをスタブする
// GET /api/users が呼び出されたら、カスタムデータを返す
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }],
}).as('getUsers'); // エイリアスを付けて後で待機・検証できるようにする
// アプリケーションがユーザーデータをフェッチする操作を実行
cy.visit('/');
cy.wait('@getUsers'); // 'getUsers'リクエストが完了するまで待機
cy.contains('Alice').should('be.visible');
例2: POSTリクエストをスパイする
// POST /api/login を監視し、レスポンスをそのまま通す
cy.intercept('POST', '/api/login').as('loginRequest');
// ログインフォームを送信する操作
cy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('#loginButton').click();
// 'loginRequest'リクエストの完了を待機し、その内容を検証
cy.wait('@loginRequest').its('request.body').should('deep.equal', {
username: 'testuser',
password: 'password123',
});
例3: エラーレスポンスをシミュレートする
// GET /api/data がエラーを返すようにする
cy.intercept('GET', '/api/data', {
statusCode: 500,
body: { message: 'Server Error' },
}).as('getDataError');
// アプリケーションがデータをフェッチする操作を実行
cy.visit('/');
cy.wait('@getDataError');
cy.contains('エラーが発生しました').should('be.visible');
例4: 遅延をシミュレートする
// GET /api/items に3秒の遅延を追加
cy.intercept('GET', '/api/items', (req) => {
req.reply({
statusCode: 200,
body: [{ id: 101, name: 'Item A' }],
delay: 3000 // 3秒の遅延
});
}).as('getItemsWithDelay');
// アプリケーションがアイテムをフェッチする操作を実行
cy.visit('/');
cy.get('.loading-spinner').should('be.visible'); // スピナーが表示されることを確認
cy.wait('@getItemsWithDelay');
cy.get('.loading-spinner').should('not.be.visible'); // スピナーが消えることを確認
Cypress intercept
の一般的なエラーとトラブルシューティング
エラー: cy.intercept() がリクエストを捕捉しない (Intercepting too late)
問題: cy.intercept()
を設定する前に、アプリケーションがすでにネットワークリクエストを発行してしまっている。Cypress のテストは非常に速く実行されるため、UIが表示されきる前にリクエストが飛んでしまうことがあります。
例:
// 誤った例
cy.visit('/'); // アプリケーションがデータをフェッチする
cy.intercept('GET', '/api/data', { body: 'mocked data' }).as('getData');
cy.wait('@getData'); // 永遠に待機してしまうか、タイムアウトする
トラブルシューティング:
cy.intercept()
は、リクエストが発行される前に設定する必要があります。
アプリケーションがリクエストを発行するトリガー(例: cy.visit()
, cy.click()
など)の前に cy.intercept()
を定義します。
// 正しい例
cy.intercept('GET', '/api/data', { body: 'mocked data' }).as('getData');
cy.visit('/'); // ここでリクエストが発行される
cy.wait('@getData'); // 成功!
エラー: cy.wait() がタイムアウトする (Timeout waiting for intercept)
問題: cy.intercept().as('aliasName')
でエイリアスを定義したが、cy.wait('@aliasName')
が設定したタイムアウト時間内にリクエストを捕捉できない。これは、前述の「Intercepting too late」の問題や、URLのマッチングが正しくない場合に発生します。
トラブルシューティング:
- リクエストがインターセプトされる順序: 複数の
cy.intercept()
が同じURLにマッチする場合、最初に定義されたインターセプトが優先されます。意図しないインターセプトが先に適用されていないか確認してください。 - メソッドの確認:
GET
,POST
などの HTTP メソッドが一致しているか確認します。 - URL のマッチングを確認:
cy.intercept()
のurl
引数が、アプリケーションが発行するリクエストの URL と正確に一致しているか確認します。- パスの先頭に
/
: 絶対パスを指定する場合、/
から始まることを確認します。 - ワイルドカード (
*
) の使用: 部分一致させたい場合は、Glob パターン (/api/*
,**/users/**
) や正規表現を使用します。cy.intercept('GET', '/api/users/*').as('getUsers'); // 例: /api/users/123 にマッチ cy.intercept('GET', /\/api\/users\/\d+/).as('getSpecificUser'); // 正規表現
- クエリパラメータの考慮:
?
以降のクエリパラメータもURLの一部として扱われるため、それらを無視したい場合はワイルドカードや正規表現を適切に使う必要があります。
- パスの先頭に
- リクエストが本当に発行されているか確認: Cypress のテストランナーの「Network」タブで、テスト中に目的のリクエストが実際に発行されているか確認します。
エラー: 期待通りのレスポンスが返ってこない (Incorrect response body/status)
問題: cy.intercept()
でスタブしたはずのデータやステータスコードが、アプリケーション側で正しく反映されていない。
トラブルシューティング:
- アプリケーション側の処理を確認: フロントエンドのコードで、APIレスポンスのパースや状態更新が正しく行われているかデバッグします。ブラウザの開発者ツール(F12)のネットワークタブも併用すると、Cypress が介入した後の実際のネットワーク通信を確認できます。
- Cypress のログを確認: テスト実行後、Cypress のコマンドログで、該当する
intercept
エイリアスをクリックします。詳細が表示され、実際のレスポンスやリクエストの内容を確認できます。ここで、Cypress がどのレスポンスを返したかがわかります。 - ハンドラーの設定を確認:
cy.intercept(method, url, handler)
のhandler
部分が正しい形式で指定されているか確認します。- オブジェクト形式:
{ statusCode: 200, body: { data: '...' } }
- 関数形式:
(req) => { req.reply({ statusCode: 200, body: '...' }) }
- オブジェクト形式:
エラー: 同じリクエストが複数回飛ぶ (Multiple requests for the same resource)
問題: 特定の操作で同じリクエストが複数回発生し、cy.intercept()
が意図しない回数だけ捕捉される。
トラブルシューティング:
- Cypress のテスト設計:
cy.intercept()
は一度設定すると、マッチするすべてのアクションに適用されます。もし特定の一回のリクエストだけをスタブしたい場合は、req.continue()
を使って一度だけ処理し、その後のリクエストは通常通り通すなどの工夫が必要です。let count = 0; cy.intercept('GET', '/api/data', (req) => { if (count === 0) { req.reply({ body: 'first data' }); count++; } else { req.continue(); // 二回目以降はそのまま通す } }).as('getData');
- アプリケーションコードの確認: 同じリクエストが複数回発行される原因がアプリケーション側にある場合があります(例: 不要なリレンダリング、イベントハンドラの二重登録など)。
エラー: cy.intercept() の後に cy.get('@aliasName').its('response.body') が null になる
問題: cy.wait('@aliasName')
の後で cy.get('@aliasName')
を使ってインターセプトの詳細を取得しようとすると null
になる。
トラブルシューティング:
cy.wait('@aliasName')
コマンドは、完了したインターセプトに関する情報をエイリアスとして自動的に生成します。したがって、cy.get('@aliasName')
を使って再度取得する必要はありません。cy.wait('@aliasName')
の戻り値として、インターセプトオブジェクトが返されるため、それを .then()
で受け取って処理します。
cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/');
cy.wait('@getUsers').then((interception) => {
// interception オブジェクトには、request, response, state などの情報が含まれる
expect(interception.response.body).to.have.length(2);
});
// cy.get('@getUsers') は避ける
CORS エラー (Cross-Origin Resource Sharing)
問題: cy.intercept()
を使用していると、CORS (Cross-Origin Resource Sharing) に関連するエラーがブラウザコンソールに表示されることがある。これはCypressがプロキシとして機能するため、特に異なるオリジンへのリクエストをインターセプトしようとすると発生しやすいです。
トラブルシューティング: Cypress は通常、CORS の問題を内部で処理します。このエラーが発生する場合、以下を確認します。
-
Webセキュリティ設定: ごく稀に、ブラウザのセキュリティ設定や企業ネットワークのプロキシなどが干渉している場合があります。
-
modifyObstructiveCode
オプション: Cypress の設定ファイル (cypress.config.js
またはcypress.json
) でmodifyObstructiveCode: false
と設定している場合、CORS関連の問題を引き起こす可能性があります。デフォルトはtrue
です。// cypress.config.js const { defineConfig } = require('cypress'); module.exports = defineConfig({ e2e: { setupNodeEvents(on, config) { // implement node event listeners here }, // 必要に応じて設定を調整 // modifyObstructiveCode: true, // デフォルトでtrue }, });
-
Cypress のバージョン: 比較的古いバージョンのCypressを使用している場合、最新バージョンにアップグレードすることで解決する場合があります。
環境依存の問題 (CI/CD とローカル環境での挙動の違い)
問題: ローカル環境では問題なく動作する intercept
が、CI/CD環境(例: GitHub Actions, Jenkins)では失敗する。
トラブルシューティング:
- ヘッドレスモード: CI環境では通常ヘッドレスモードで実行されます。ヘッドレスモード特有の挙動(例:
window.fetch
のpolyfill)が影響している可能性も考慮します。 - タイムアウト: CI環境ではネットワーク遅延が発生しやすいため、
cy.wait()
のタイムアウト値を長めに設定することを検討します。 - ネットワーク設定: CI環境のファイアウォールやプロキシが、Cypressからのネットワークリクエストをブロックしていないか確認します。
- ベースURL:
cypress.config.js
のbaseUrl
がローカルとCIで適切に設定されているか確認します。 - 環境変数: APIのエンドポイントなどが環境変数に依存している場合、CI環境で正しく設定されているか確認します。
- ブラウザの開発者ツール: Cypress のテストが実行されているブラウザ(Electron など)の開発者ツール(F12)を開き、ネットワークタブで実際のHTTPリクエスト/レスポンスを確認します。Cypress の
intercept
を通った後の通信がどうなっているかを確認できます。 - Cypress のネットワークタブ: Cypress テストランナーの左側のペインにある「Network」タブを常に確認し、リクエストが期待通りに飛んでいるか、どの
intercept
が適用されたかを確認します。 cy.log()
やconsole.log()
:cy.intercept()
のハンドラー関数内でconsole.log()
を使って、リクエストやレスポンスの内容をコンソールに出力し、デバッグします。- Cypress UI のコマンドログ: テスト実行中にCypressのUIでコマンドログを確認します。
intercept
コマンドをクリックすると、詳細なリクエスト/レスポンス情報が表示されます。
Cypress cy.intercept()
プログラミング例
cy.intercept()
は、アプリケーションが発行するネットワークリクエスト(HTTP/HTTPS)を捕捉し、スタブ、モック、スパイ、または変更するために使用されます。これにより、フロントエンドのテストをより制御可能で、高速で、安定したものにできます。
基本的な使い方
cy.intercept(method, url, handler)
handler
: リクエストが捕捉されたときに実行される処理。これはオブジェクト(スタブデータを直接返す場合)または関数(より複雑なロジックやスパイの場合)です。url
: リクエストの URL。文字列、Glob パターン、または正規表現を使用できます。method
: HTTPメソッド(例:'GET'
,'POST'
,'PUT'
,'DELETE'
,'PATCH'
,'*'
)。'*'
は任意のメソッドにマッチします。
例1: GET リクエストをスタブする (固定データを返す)
最も一般的な使用例です。特定の GET リクエストに対して、事前定義された JSON データを返します。これにより、バックエンドが利用できない場合でも、または特定のデータ状態をテストしたい場合に、UI の表示をテストできます。
// feature-users.spec.js
describe('ユーザーリストの表示', () => {
beforeEach(() => {
// cy.intercept() はリクエストが発行される前に設定する必要がある
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: '山田 太郎', email: '[email protected]' },
{ id: 2, name: '田中 花子', email: '[email protected]' },
],
delayMs: 100, // 任意:少し遅延を入れてローディング表示をテストする
}).as('getUsers'); // このインターセプトに 'getUsers' というエイリアスを付ける
});
it('APIから取得したユーザーをリスト表示する', () => {
cy.visit('/users'); // /users ページにアクセス。ここで /api/users が呼び出されると想定
cy.wait('@getUsers'); // 'getUsers' エイリアスが完了するまで待機(スタブされたレスポンスが返る)
// UI 上にユーザーが表示されていることを確認
cy.contains('山田 太郎').should('be.visible');
cy.contains('田中 花子').should('be.visible');
cy.get('ul > li').should('have.length', 2); // ユーザーが2人いることを確認
});
it('ユーザーデータが空の場合にメッセージを表示する', () => {
// このテストでは、getUsers インターセプトを上書きして空のデータを返す
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [],
}).as('getEmptyUsers');
cy.visit('/users');
cy.wait('@getEmptyUsers');
cy.contains('ユーザーがいません。').should('be.visible');
cy.get('ul > li').should('not.exist');
});
});
例2: POST リクエストをスパイする (リクエスト内容の検証)
アプリケーションが送信するリクエストのボディやヘッダーを検証したい場合に利用します。実際のリクエストはバックエンドに送信させつつ、テスト側でその内容をチェックできます。
// feature-login.spec.js
describe('ログイン機能', () => {
it('正しい認証情報でログインできる', () => {
// POST /api/login リクエストを傍受し、'loginRequest' というエイリアスを付ける
// このインターセプトは、リクエストを通常通り通過させる(スタブしない)
cy.intercept('POST', '/api/login').as('loginRequest');
cy.visit('/login');
cy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('button[type="submit"]').click();
// 'loginRequest' が完了するまで待機し、そのインターセプトオブジェクトを取得
cy.wait('@loginRequest').then((interception) => {
// リクエストのボディを検証
expect(interception.request.body).to.deep.equal({
username: 'testuser',
password: 'password123',
});
// レスポンスのステータスコードを検証 (バックエンドが返すもの)
expect(interception.response.statusCode).to.equal(200);
// レスポンスのボディを検証
expect(interception.response.body).to.have.property('token');
});
// ログイン後の画面遷移や表示を確認
cy.url().should('include', '/dashboard');
cy.contains('ようこそ、testuserさん!').should('be.visible');
});
it('無効な認証情報でログインに失敗する', () => {
// ログインAPIが401エラーを返すようにスタブ
cy.intercept('POST', '/api/login', {
statusCode: 401,
body: { message: '無効なユーザー名またはパスワードです。' },
}).as('failedLogin');
cy.visit('/login');
cy.get('#username').type('wronguser');
cy.get('#password').type('wrongpass');
cy.get('button[type="submit"]').click();
cy.wait('@failedLogin');
// エラーメッセージが表示されることを確認
cy.contains('無効なユーザー名またはパスワードです。').should('be.visible');
// ログインページに留まっていることを確認
cy.url().should('include', '/login');
});
});
例3: 遅延をシミュレートする (ローディング状態のテスト)
ネットワークの遅延を意図的に作り出し、ローディングスピナーやプレースホルダーなどが適切に表示されるかをテストします。
// feature-loading.spec.js
describe('ローディング状態の表示', () => {
it('データフェッチ中にローディングインジケーターが表示される', () => {
// GET /api/items に5秒の遅延を追加
cy.intercept('GET', '/api/items', (req) => {
// req.reply() を関数形式で使うと、より柔軟な処理が可能
req.reply({
statusCode: 200,
body: [{ id: 1, name: 'Item A' }],
delay: 5000, // 5000ミリ秒 = 5秒の遅延
});
}).as('getItemsWithDelay');
cy.visit('/items');
// 遅延中にローディングスピナーが表示されていることを確認
cy.get('.loading-spinner').should('be.visible');
cy.wait('@getItemsWithDelay'); // 遅延が完了するまで待機
// 遅延完了後にスピナーが非表示になり、データが表示されていることを確認
cy.get('.loading-spinner').should('not.exist');
cy.contains('Item A').should('be.visible');
});
});
例4: エラーレスポンスをシミュレートする (エラーハンドリングのテスト)
API からエラーレスポンスが返ってきた場合の、アプリケーションのエラーハンドリングやUI表示をテストします。
// feature-error.spec.js
describe('エラーハンドリング', () => {
it('APIエラー時にエラーメッセージを表示する', () => {
// GET /api/products が500エラーを返すようにスタブ
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { message: 'サーバーで問題が発生しました。' },
}).as('getProductsError');
cy.visit('/products');
cy.wait('@getProductsError');
// エラーメッセージが表示されていることを確認
cy.contains('サーバーで問題が発生しました。').should('be.visible');
// プロダクトリストが表示されていないことを確認
cy.get('.product-list').should('not.exist');
});
});
例5: リクエスト URL のパターンマッチング
cy.intercept()
の url
引数には、文字列の他に Glob パターンや正規表現を使用できます。
// feature-dynamic-routes.spec.js
describe('動的なAPIルートのテスト', () => {
it('特定のIDのユーザーデータをスタブする (Glob パターン)', () => {
// /api/users/123 のようなURLにマッチ
cy.intercept('GET', '/api/users/*', {
statusCode: 200,
body: { id: 999, name: 'モックユーザー', email: '[email protected]' },
}).as('getAnyUser');
cy.visit('/user/1'); // アプリケーションが /api/users/1 を呼び出すと仮定
cy.wait('@getAnyUser');
cy.contains('モックユーザー').should('be.visible');
cy.visit('/user/500'); // アプリケーションが /api/users/500 を呼び出すと仮定
cy.wait('@getAnyUser');
cy.contains('モックユーザー').should('be.visible');
});
it('正規表現を使って特定のIDのユーザーデータをスタブする', () => {
// /api/users/ の後に数字が続くURLにマッチ
// 例: /api/users/123 はマッチするが、/api/users/abc はマッチしない
cy.intercept('GET', /\/api\/users\/\d+$/, {
statusCode: 200,
body: { id: 100, name: 'Regex User', email: '[email protected]' },
}).as('getSpecificUser');
cy.visit('/user/123'); // /api/users/123 が呼び出される
cy.wait('@getSpecificUser');
cy.contains('Regex User').should('be.visible');
// cy.visit('/user/abc'); // これはマッチしない場合がある(正規表現による)
// その場合は実際のAPIコールが行われるか、404になるかを確認する
});
});
cy.intercept()
のハンドラー関数は、req
オブジェクトを引数として受け取ります。これを使って、リクエストの内容に基づいて動的に応答を変更したり、特定の条件下でリクエストを通過させたりできます。
// feature-conditional-intercept.spec.js
describe('条件付きインターセプト', () => {
it('特定のクエリパラメータを持つリクエストだけをスタブする', () => {
cy.intercept('GET', '/api/search*', (req) => {
// req.query でクエリパラメータにアクセスできる
if (req.query.q === 'cypress') {
req.reply({
statusCode: 200,
body: [{ id: 1, title: 'Cypress入門' }],
});
} else {
// 条件に合わないリクエストはそのまま通過させる
req.continue();
}
}).as('search');
cy.visit('/search');
// 'cypress' で検索した場合
cy.get('#search-input').type('cypress');
cy.get('#search-button').click();
cy.wait('@search');
cy.contains('Cypress入門').should('be.visible');
// 'react' で検索した場合 (実際のAPIコールが通過する)
cy.get('#search-input').clear().type('react');
cy.get('#search-button').click();
// ここでは実際のバックエンドからのレスポンスを期待する
// 例: cy.wait('@search'); としても良いが、その場合も continue() が呼ばれたログが残る
// 実際のアプリケーションコードで、検索結果が表示されることを確認
cy.contains('Reactコンポーネント').should('be.visible'); // 仮の実際のAPIレスポンス
});
it('リクエストヘッダーに基づいてレスポンスを分ける', () => {
cy.intercept('GET', '/api/dashboard', (req) => {
if (req.headers['x-admin-user'] === 'true') {
req.reply({
statusCode: 200,
body: { message: '管理者ダッシュボードデータ' },
});
} else {
req.reply({
statusCode: 200,
body: { message: '一般ユーザーダッシュボードデータ' },
});
}
}).as('getDashboard');
// 管理者としてログインした後のダッシュボード表示をテスト
// (アプリケーションが 'x-admin-user' ヘッダーをセットする動作を仮定)
cy.visit('/dashboard?user=admin'); // 管理者としてアクセスする仮のURL
cy.wait('@getDashboard');
cy.contains('管理者ダッシュボードデータ').should('be.visible');
// 一般ユーザーとしてログインした後のダッシュボード表示をテスト
cy.visit('/dashboard?user=normal'); // 一般ユーザーとしてアクセスする仮のURL
cy.wait('@getDashboard');
cy.contains('一般ユーザーダッシュボードデータ').should('be.visible');
});
});
cy.intercept()
は非常に強力で柔軟な機能ですが、すべてのシナリオで唯一の解決策というわけではありません。状況に応じて、他の方法がより適切であったり、シンプルであったりする場合があります。
cy.request() を使って直接 API を叩く
これは cy.intercept()
の直接的な代替ではありませんが、テストの準備段階でデータの状態を操作する目的でよく使われます。cy.request()
は Cypress のテストコードから直接 HTTP リクエストを送信するコマンドです。
cy.intercept()
との違い:
cy.intercept()
はフロントエンドとバックエンド間の通信を制御するのに対し、cy.request()
はバックエンドの状態を直接操作するために使われます。cy.intercept()
はアプリケーションが発行するリクエストを傍受するのに対し、cy.request()
はテストコード自身がリクエストを送信します。
使用例: テストの開始前に、DB をクリーンアップしたり、特定のテストデータを作成したりする場合に非常に便利です。UI を介さずに直接 API を叩くことで、テストのセットアップが高速化されます。
describe('商品購入機能', () => {
beforeEach(() => {
// テスト開始前に、必要な商品データがDBにあることを保証する
cy.request('POST', '/api/test/reset-db'); // DBをリセットするAPIを叩く
cy.request('POST', '/api/products', { name: 'テスト商品', price: 1000, stock: 5 }); // 商品を作成するAPIを叩く
// UI にアクセス
cy.visit('/products');
});
it('商品をカートに追加し、購入できる', () => {
cy.contains('テスト商品').click();
cy.contains('カートに追加').click();
cy.url().should('include', '/cart');
// ...購入処理を続ける
});
});
メリット:
- 特定のリクエストのみをテストし、そのレスポンスを検証する際に便利。
- UI レイヤーをスキップして、テストのセットアップやティアダウンを高速化できる。
- 実際のバックエンドとの通信をテストできる。
デメリット:
- エラーレスポンスやローディング状態など、ネットワークの振る舞いをシミュレートすることは難しい。
- アプリケーションの UI が発行するリクエストをコントロールすることはできない。
ファイルシステムからフィクスチャを読み込む (cy.fixture())
これは cy.intercept()
の中で頻繁に利用されるテクニックですが、cy.fixture()
自体は代替手段ではありません。しかし、API レスポンスをモックするために cy.intercept()
と組み合わせて使うことで、テストデータの管理を効率化できます。
使用例:
API レスポンスの JSON データが大きい場合や、複数のテストで同じデータを使い回す場合に、cypress/fixtures
ディレクトリに JSON ファイルとして保存し、cy.fixture()
で読み込みます。
cypress/fixtures/users.json
:
[
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
テストコード:
describe('ユーザーリスト', () => {
it('フィクスチャからユーザーデータを読み込んで表示する', () => {
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.contains('Alice').should('be.visible');
cy.contains('Bob').should('be.visible');
});
});
メリット:
- 複数のテストケースで同じデータを簡単に再利用できる。
- 大規模な JSON データでもコードを汚さずに扱える。
- テストデータがクリーンに分離され、管理しやすい。
デメリット:
- 動的なデータ生成や、リクエストの内容に基づいた複雑なレスポンスの変更には不向き。
Cypress の設定ファイルでグローバルにネットワークを制御する (非推奨/限定的)
非常に限定的なケースですが、Cypress の設定ファイル (cypress.config.js
または cypress.json
) で baseUrl
などを設定することで、テスト対象のアプリケーションがアクセスするベース URL を定義できます。これはネットワークリクエストの直接的な制御ではありませんが、テスト環境の切り替えには役立ちます。
cypress.config.js
(または cypress.json
) の例:
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000', // アプリケーションのベースURL
// ...その他の設定
},
});
メリット:
- 環境ごとのURLの切り替えが容易になる。
デメリット:
- リクエストのスタブ、スパイ、モックといった
intercept
の主要な機能は提供しない。
バックエンドのモックサーバーやサービスを使う
Cypress とは独立したバックエンドのモックサーバー (例: Mock Service Worker (MSW), json-server, WireMock など) を利用する方法です。
cy.intercept()
との違い:
cy.intercept()
はテストコード内で直接定義するが、モックサーバーはアプリケーションの一部として、または独立したプロセスとして起動します。cy.intercept()
が Cypress テストランナーの内部でネットワークリクエストを傍受するのに対し、モックサーバーは実際のバックエンド API の代わりに動作します。
使用例: MSW (Mock Service Worker) は、ブラウザの Service Worker API を利用してネットワークリクエストを捕捉・モックするため、Cypress のテストだけでなく、開発時や Storybook などでも同じモックロジックを共有できます。
// MSW の設定例 (これはCypressのコードではないが、概念の理解のために記載)
// src/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ id: 1, name: 'MSW太郎' },
{ id: 2, name: 'MSW花子' },
])
);
}),
rest.post('/api/login', (req, res, ctx) => {
const { username, password } = req.body;
if (username === 'test' && password === 'pass') {
return res(ctx.status(200), ctx.json({ token: 'mock-token' }));
}
return res(ctx.status(401), ctx.json({ message: 'Invalid credentials' }));
}),
];
// テストコード側では、MSW が起動している前提で通常通り訪問する
describe('MSW を使ったテスト', () => {
it('モックされたユーザーリストが表示される', () => {
cy.visit('/users');
cy.contains('MSW太郎').should('be.visible');
cy.contains('MSW花子').should('be.visible');
});
});
メリット:
- バックエンドチームが API を提供する前にフロントエンド開発を進められる。
- Cypress に依存しない: Cypress の外でもモック機能を利用できる。
- 開発とテストの一貫性: 開発時にも同じモックデータを使用できるため、モックと実際のAPIの乖離が少ない。
デメリット:
- テストケースごとにモックの振る舞いを動的に変更するのが、
cy.intercept()
ほど簡単ではない場合がある(特にシンプルなスタブの場合)。 - セットアップが
cy.intercept()
より複雑になる場合がある。
ローカルストレージやセッションストレージを直接操作する
API から返されるデータが、セッション情報やユーザー設定など、永続化されるものの場合、直接ローカルストレージやセッションストレージを操作して、その状態をシミュレートできます。
使用例:
describe('ユーザー設定のテスト', () => {
beforeEach(() => {
// ログイン状態をシミュレートするためにトークンをセット
cy.window().then((win) => {
win.localStorage.setItem('authToken', 'mock_jwt_token');
win.localStorage.setItem('userSettings', JSON.stringify({ theme: 'dark', notifications: true }));
});
cy.visit('/settings');
});
it('ユーザー設定が正しく表示される', () => {
cy.get('#theme-selector').should('have.value', 'dark');
cy.get('#notifications-checkbox').should('be.checked');
});
});
メリット:
- シンプルな状態のシミュレーションには効果的。
- API 呼び出しを完全に回避できるため、非常に高速。
デメリット:
- テスト対象のアプリケーションが API 呼び出しではなく、ストレージから直接データを読み込む場合にのみ有効。
- 複雑な API レスポンスや、ネットワークエラー、遅延などのシナリオをシミュレートできない。
cy.intercept()
は Cypress におけるネットワークリクエストの制御において最も柔軟で包括的なアプローチですが、上記のような代替手段も、特定のニーズやテストの範囲に応じて有効な選択肢となります。
- シンプルな状態のシミュレーション: ローカルストレージなどを直接操作する。
- 開発とテストで一貫したモック: MSW などのバックエンドモックサーバーを検討する。
- 複雑なモックデータの管理:
cy.fixture()
とcy.intercept()
を組み合わせる。 - テスト環境の準備やデータ作成:
cy.request()
を使う。 - 簡単なAPIレスポンスのスタブやスパイ:
cy.intercept()
が最適。