Mirage を使用した Cypress でのモックネットワークリクエスト

Cypress を使用して、さまざまなサーバーシナリオでアプリケーションをテストするために Mirage サーバーを使用します。

これは、アプリケーションで既に Cypress を使用しているユーザー向けのクイックスタートガイドです。

ステップ1:Mirage のインストール

まず、Mirage がインストールされていることを確認してください。

# Using npm
npm install --save-dev miragejs

# Using Yarn
yarn add --dev miragejs

ステップ2:サーバーの定義

新しいsrc/server.jsファイルを作成し、モックサーバーを定義します。

基本的な例を以下に示します。

// src/server.js
import { createServer, Model } from "miragejs"

export function makeServer({ environment = "development" } = {}) {
  let server = createServer({
    environment,

    models: {
      user: Model,
    },

    seeds(server) {
      server.create("user", { name: "Bob" })
      server.create("user", { name: "Alice" })
    },

    routes() {
      this.namespace = "api"

      this.get("/users", (schema) => {
        return schema.users.all()
      })
    },
  })

  return server
}

ステップ3:アプリケーションの API リクエストに対するプロキシ関数を Cypress で定義する

次のコードをcypress/support/index.jsファイルに追加します。

// cypress/support/index.js
Cypress.on("window:before:load", (win) => {
  win.handleFromCypress = function (request) {
    return fetch(request.url, {
      method: request.method,
      headers: request.requestHeaders,
      body: request.requestBody,
    }).then((res) => {
      let content = res.headers.get("content-type").includes("application/json")
        ? res.json()
        : res.text()
      return new Promise((resolve) => {
        content.then((body) => resolve([res.status, res.headers, body]))
      })
    })
  }
})

このコードは、アプリケーションのwindowオブジェクトにhandleFromCypress関数を定義します。次のステップでは、Cypress が実行されている間にネットワークリクエストを行うたびに、この関数を呼び出すようにアプリケーションを構成します。

ステップ4:アプリケーションのネットワークリクエストをプロキシする

アプリケーションのブートストラップファイルで、Cypress が実行されている場合、Mirage を使用してアプリケーションの API リクエストを前のステップで定義したhandleFromCypress関数にプロキシします。

Create React App ユーザーの場合、このコードはsrc/index.jsに入ります。

Vue CLI ユーザーの場合、このコードはsrc/main.jsに入ります。

import { createServer, Response } from "miragejs"

if (window.Cypress) {
  // If your app makes requests to domains other than / (the current domain), add them
  // here so that they are also proxied from your app to the handleFromCypress function.
  // For example: let otherDomains = ["https://my-backend.herokuapp.com/"]
  let otherDomains = []
  let methods = ["get", "put", "patch", "post", "delete"]

  createServer({
    environment: "test",
    routes() {
      for (const domain of ["/", ...otherDomains]) {
        for (const method of methods) {
          this[method](`${domain}*`, async (schema, request) => {
            let [status, headers, body] = await window.handleFromCypress(
              request
            )
            return new Response(status, headers, body)
          })
        }
      }

      // If your central server has any calls to passthrough(), you'll need to duplicate them here
      // this.passthrough('https://analytics.google.com')
    },
  })
}

これで、Cypress がアプリケーションを起動するたびに、このコードはアプリケーションのネットワークリクエストを前のステップで定義したhandleFromCypress関数に委任します。

実際の構成済みの Mirage サーバーを Cypress コードと一緒に起動すると、その関数からのリクエストのインターセプトを開始します。

ステップ5:Mirage サーバーを使用してテストを作成する

新しいcypress/integration/app.spec.jsファイルを作成し、makeServer関数をインポートし、各テストの前後に Mirage を起動およびシャットダウンします。その後、各テストで異なるデータシナリオを使用して Mirage にシードし、テストを使用して UI の状態を検証できます。

import { makeServer } from "../../src/server"

describe("user list", () => {
  let server

  beforeEach(() => {
    server = makeServer({ environment: "test" })
  })

  afterEach(() => {
    server.shutdown()
  })

  it("shows the users from our server", () => {
    server.create("user", { id: 1, name: "Luke" })
    server.create("user", { id: 2, name: "Leia" })

    cy.visit("/")

    cy.get('[data-testid="user-1"]').contains("Luke")
    cy.get('[data-testid="user-2"]').contains("Leia")
  })

  it("shows a message if there are no users", () => {
    // Don't create any users

    cy.visit("/")

    cy.get('[data-testid="no-users"]').should("be.visible")
  })
})

Mirage ではmakeServer関数にenvironment: testオプションを渡すため、Mirage はデータベースシードを読み込みません。そのため、サーバーは各テスト実行時に空の状態から開始され、テストの開始時にserver.createを使用してデータシナリオを設定できます。テスト環境では、ログとレイテンシも無効になっているため、デフォルトで CI テストログはクリーンになり、テストは高速になります。

また、Cypress のdescribeブロックの使用にも注意してください。これにより、より多くのファイルを追加しても、Mirage サーバーは各仕様にスコープされ、テストファイル間の状態のリークを防ぎます。

ステップ6:異なるサーバーの状態をテストするために Mirage サーバーを変更する

異なるデータシナリオに加えて、テストを使用して Mirage サーバーを再構成し、新しい状況をテストできます。

たとえば、次のようにエラー状態をテストできます。

import { Response } from "miragejs"

it("handles error responses from the server", () => {
  // Override Mirage's route handler for /users, just for this test
  server.get("/users", () => {
    return new Response(500, {}, { error: "The database is on vacation." })
  })

  cy.visit("/")

  cy.get('[data-testid="server-error"]').contains(
    "The database is on vacation."
  )
})

Mirage が Cypress と統合される方法により、各テストではメインサーバーの定義に基づいて新しい Mirage サーバーが取得されます。テスト内で行ったオーバーライドは、そのテストに限定されます。