すでにCypressをエンドツーエンドテストで実行しているとします。今、UIを操作することなく、APIを直接叩き、ステータスコードをチェックし、JSONボディをアサートしたいと考えているかもしれません。Cypressはそれが可能です。cy.request() コマンドは、ブラウザの外、Nodeから実際のHTTPリクエストを送信するため、エンドポイント単体でテストしたり、UIテストが実行される前に状態をセットアップしたりすることができます。
このガイドでは、CypressでAPIをテストする方法を紹介します。具体的には、cy.request() の基本、レスポンスのアサート、認証トークンを再利用するためのリクエストのチェイン、そしてcy.intercept() を使ったブラウザトラフィックのスタブ化です。また、APIテストにCypressが適している場面と、APIネイティブツールがより適している場面についても説明します。
CypressでAPIをテストする理由
ほとんどのCypressスイートはブラウザを駆動します。しかし、UIで検証することの多くは、その下のAPIに関連しています。例えば、/login がトークンを返すか、/users が正しい形式を返すか、POST が期待するレコードを作成するか、といったことです。
これらのエンドポイントを直接テストすることには、2つの利点があります。第一に、APIチェックはUIフローよりも高速に実行されます。レンダリングがなく、要素を待つ必要がないからです。第二に、cy.request() を使用してテストデータをセットアップできます。ユーザーを作成するためにサインアップフォームをクリックして進む代わりに、1回の POST を送信し、実際に重要なアサートに移ることができます。
すでにCypressを導入している場合、APIテストを追加しても新しいツールをインストールする必要はありません。同じスペックファイル内で、同じ expect アサートを使って、同じCI実行中にテストを作成できます。
cy.request() の基本
cy.request() はHTTPリクエストを発行し、レスポンスを返します。これはブラウザではなくCypressのNodeプロセスから実行されるため、CORSや同一オリジンポリシーの制約を受けません。
このコマンドはいくつかの形式を受け入れます。
cy.request(url)
cy.request(url, body)
cy.request(method, url)
cy.request(method, url, body)
cy.request(options)
最もシンプルな呼び出しはURLを渡すものです。メソッドを指定しない場合、CypressはデフォルトでGETを使用します。
cy.request('https://jsonplaceholder.typicode.com/users')
基本的なGET以外の操作を行う場合は、オプションオブジェクトを渡します。これにより、メソッド、ボディ、ヘッダーを完全に制御できます。
cy.request({
method: 'POST',
url: 'https://jsonplaceholder.typicode.com/posts',
headers: {
'Content-Type': 'application/json'
},
body: {
title: 'API test',
body: 'created from Cypress',
userId: 1
}
})
そのオブジェクトで役立つオプション:
method: HTTP動詞(GET、POST、PUT、PATCH、DELETEなど)。デフォルトはGETです。url: エンドポイント。必須です。body: リクエストペイロード。CypressはオブジェクトをJSONにシリアル化します。headers: リクエストヘッダーのオブジェクト。auth: HTTPベーシック認証の認証情報。qs: クエリ文字列パラメータをオブジェクトとして。failOnStatusCode: テストを失敗させる代わりに、4xxまたは5xxレスポンスをアサートしたい場合にfalseに設定します。
最後のオプションはネガティブテストで重要です。デフォルトでは、cy.request() は2xxまたは3xx以外のステータスでテストを失敗させます。不正なリクエストが400を返すことを確認したい場合は、この設定を無効にする必要があります。
cy.request({
method: 'GET',
url: 'https://jsonplaceholder.typicode.com/users/99999',
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.eq(404)
})
ステータスとボディのアサート
cy.request() はレスポンスオブジェクトを返します。.then() をチェインしてそれを読み取ります。レスポンスには、主に使用する4つのプロパティがあります。
status: HTTPステータスコード。body: レスポンスペイロード。Content-Typeがjsonで終わる場合、CypressはそれをJavaScriptオブジェクトにパースします。headers: レスポンスヘッダー。duration: リクエストにかかった時間(ミリ秒単位)。
ステータス、ボディの形式、および特定のフィールドをチェックする完全なテスト例です。
describe('Users API', () => {
it('returns a list of users', () => {
cy.request('https://jsonplaceholder.typicode.com/users').then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.be.an('array')
expect(response.body).to.have.length(10)
expect(response.body[0]).to.have.property('email')
})
})
})
response.body はすでにパースされたオブジェクトであるため、標準のChaiを使ってアサートできます。型、長さ、ネストされたプロパティ、または正確な値をチェックできます。これはUIテストで使用するアサート構文と同じなので、新しく学ぶことはありません。
レスポンスヘッダーもアサートできます。これはコンテンツタイプやキャッシュのチェックに便利です。
cy.request('https://jsonplaceholder.typicode.com/users').then((response) => {
expect(response.headers['content-type']).to.include('application/json')
})
これらのチェックの構造についてさらに詳しく知るには、APIアサーションに関するガイドと、より広範なAPIテストのベストプラクティスをご覧ください。
リクエストのチェインと認証トークンの再利用
実際のAPIには認証が必要です。一般的なパターンは、一度ログインしてトークンを取得し、その後のすべてのリクエストでそれを送信することです。cy.request() はこの目的のためにきれいにチェインできます。
ログインリクエストを送信し、レスポンスボディからトークンを読み取り、次の呼び出しで使用します。
describe('Authenticated API flow', () => {
it('logs in and fetches a protected resource', () => {
cy.request({
method: 'POST',
url: 'https://api.example.com/login',
body: {
email: 'user@example.com',
password: 'secret'
}
}).then((loginResponse) => {
expect(loginResponse.status).to.eq(200)
const token = loginResponse.body.token
cy.request({
method: 'GET',
url: 'https://api.example.com/profile',
headers: {
Authorization: `Bearer ${token}`
}
}).then((profileResponse) => {
expect(profileResponse.status).to.eq(200)
expect(profileResponse.body).to.have.property('email', 'user@example.com')
})
})
})
})
複数のテストで同じトークンが必要な場合は、ログイン処理をbeforeEachに移動し、Cypress.env() またはラップされたエイリアスを使ってトークンを保存すると、各テストがそれを利用できます。これにより、各スペックで認証設定を繰り返す代わりに、一箇所にまとめることができます。
この「ログインして使用する」パターンは、1つの呼び出しが次の呼び出しにデータを供給する、あらゆるAPI統合テストフローで記述するのと同じ形です。
スタブ化のための cy.intercept()
cy.request() は実際のAPIを叩きます。しかし、時にはその逆を望むことがあります。つまり、フロントエンドアプリが行うリクエストを傍受し、偽のレスポンスを返したい場合です。それが cy.intercept() です。
ここに重要な区別があります。cy.intercept() は、ブラウザでフロントエンドアプリケーションが行うリクエストのみを傍受します。cy.request() はブラウザを完全にバイパスするため、cy.intercept() は cy.request() を傍受しません。cy.intercept() は、UIが特定のAPIレスポンスにどのように反応するかをテストする場合に使用し、API自体をテストする場合には使用しません。
リクエストをスパイしたり、静的なレスポンスでスタブしたり、待機したりすることができます。スタブするには、レスポンスオブジェクトを渡します。
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [{ id: 1, name: 'Ada' }]
})
これで、/api/users へのブラウザリクエストは、その固定ペイロードを返します。これにより、空のリスト、500 エラー、遅いレスポンスなど、実際のバックエンドではトリガーしにくいエッジケースをテストできます。
リクエストが発生したことをアサートするには、.as() でエイリアスを付けて、それを待ちます。
it('shows an error banner when the API fails', () => {
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { message: 'Server error' }
}).as('getUsers')
cy.visit('/dashboard')
cy.wait('@getUsers').its('response.statusCode').should('eq', 500)
cy.contains('Something went wrong').should('be.visible')
})
cy.wait('@getUsers') の行は、傍受されたリクエストが解決されるまでテストを一時停止し、その後、アサートするためのリクエストとレスポンスを渡します。リクエストをトリガーするアクションの前にインターセプトを設定しないと、何もキャッチされません。レスポンスを偽装すべきか、サービス全体をモックすべきか検討している場合は、APIモックとAPIスタブ vs APIモックに関する記事でトレードオフを詳しく解説しています。
APIテストにCypressが適している場面と適さない場面
Cypressは、UIテストスイート内で行われるAPIチェックに非常に適しています。すでにE2Eテストを作成しており、データのシード、UIが依存するエンドポイントの検証、エラー処理をテストするためのレスポンスのスタブ化が必要な場合、cy.request() と cy.intercept() を使えばすべてを一箇所にまとめることができます。コンテキストの切り替えも、別のツールも不要です。
また、Cypressが唯一のテストランナーであり、別の依存関係を追加したくない場合に、いくつかのスタンドアロンAPIスモークテストを行うのにも適しています。
Cypressが不便になるのは、大規模なスタンドアロンAPIテストスイートの場合です。Cypressはブラウザ向けに構築されており、APIテストはコア設計ではなく、その上にレイヤーとして追加された機能です。スイートが大きくなるにつれて、いくつかのギャップが明らかになります。
- 視覚的なリクエストビルダーがない。すべてのリクエストはコードです。数百のエンドポイントがある場合、ヘッダー、ボディ、クエリパラメータを手作業で作成するのは時間がかかります。
- CI専用のAPIパイプラインには弱い。Cypressは純粋なAPI実行の場合でもブラウザコンテキストを起動するため、不要なオーバーヘッドが発生します。
- 共有されたAPI定義がない。リクエストの形式はテストファイル内にあり、設計、ドキュメント、テスト全体でチームが再利用できる仕様にはありません。
これはAPIネイティブツールがより適している場面です。ApidogはAPI自体を中心に構築されています。エンドポイントを一度設計すれば、視覚的なリクエストビルダーと視覚的なアサートを使ってテストシナリオを構築でき、一般的なケースではコードは不要です。リクエスト、環境、認証は共有ワークスペースに存在するため、チームがテストファイルからそれらを再導出する必要はありません。
CIの場合、Apidog CLIは、Nodeを実行できるあらゆるステップで、保存されたテストシナリオをヘッドレスで実行します。
npm install -g apidog-cli
apidog run \
--access-token "$APIDOG_ACCESS_TOKEN" \
-t <scenarioOrSuiteId> \
-e <environmentId> \
-r cli,html,junit
-t フラグは保存されたシナリオまたはスイートを対象とし、-e は環境を選択し、-r はレポート形式(cli、html、json、junit)を選択します。JUnit出力は、ほとんどのCIダッシュボードに直接組み込むことができます。データファイルからテストを駆動するには -d または --iteration-data を追加し、結果をワークスペースにプッシュするには --upload-report を追加します。CLIは保存されたシナリオを実行するものであり、インタラクティブなリクエスト送信ツールではありません。
これを考えるのに役立つ方法としては、ブラウザテストをサポートするAPIチェックにはcy.request()を使用し、APIテストが主要な作業である場合はAPIネイティブツールに手を伸ばす、というものです。オプションを比較している場合、CI/CDで実行されるAPIテスト自動化ツールと、より広範な30のベストAPIテストツールに関する当社のまとめ記事が参考になるでしょう。
FAQ
cy.request() はブラウザで実行されますか?
いいえ。cy.request() はブラウザの外、CypressのNodeプロセスから実行されます。そのため、CORSや同一オリジンポリシーによってブロックされず、cy.intercept() がそれを傍受できない理由でもあります。ブラウザが行うリクエストが必要な場合は、アプリを通じてそれをトリガーし、cy.intercept() で捕捉してください。
テストを失敗させずに400または404レスポンスをテストするにはどうすればよいですか?
デフォルトでは、cy.request() は2xxまたは3xx以外のステータスで失敗します。オプションオブジェクトでfailOnStatusCode: falseを設定し、その後expect(response.status).to.eq(404)を使用して自分でステータスをアサートしてください。
UIテストの前に cy.request() を使ってログインできますか?
はい、それは一般的なパターンです。cy.request() でログインエンドポイントにPOSTを送信し、response.body からトークンを読み取り、それを保存(Cypress.env()、localStorage、またはクッキーに)することで、UIテストがすでに認証された状態で開始できます。これにより、ログインフォームをスキップし、スイートの速度を向上させることができます。
cy.request() と cy.intercept() の違いは何ですか?
cy.request() は実際のHTTPリクエストを送信し、実際のレスポンスを返します。そのため、APIをテストするために使用します。一方、cy.intercept() は、フロントエンドがブラウザで行うリクエストを監視または偽装するため、UIが受け取るものを制御するために使用します。一方はネットワークを叩き、もう一方はそれを形作ります。
APIテストにはCypressと専用ツールのどちらを使うべきですか?
すでに実行しているブラウザテストスイートをAPIチェックでサポートする場合にCypressを使用してください。大規模なスタンドアロンAPIスイート、CI専用のAPIパイプライン、またはチーム全体が作業する共有API定義が必要な場合は、ApidogのようなAPIネイティブツールがより適しています。どちらを選択するかについては、APIテストのベストプラクティスをご覧ください。
