アプリケーションテスト

Mirageの最も優れている点の1つは、JavaScriptアプリ開発者が、本番APIサーバーとやり取りする必要なく、高レベルのUIテストを作成できることです。

これらのテストは、アプリケーションのユーザフローの検証に関係します。この場合のユーザーとは、コンピューターやモバイルデバイスでWebアプリを使用し、キーボード、マウス、その他の入力デバイスを操作する実際の人物です。したがって、これらのテストは、これらの人々が現実世界でアプリケーションとやり取りする方法と密接に似ている必要があります。

例を考えてみましょう。テストしたい次のユーザフローを見てください。

ユーザーはホームページにアクセスすると映画のリストを表示できる

このようなアプリケーションテストのほとんどは、たとえ省略したり暗黙的なままにしたりしても、特定のサーバー状態に依存しています。そして、ここでMirageが登場します。これにより、サーバーの状態をテストの明示的な一部にすることができます。

上記のテストをより完全なものにするために書き換えるとしたら、次のようになるかもしれません。

  • 前提として、サーバーに10個の映画リソースが存在する
  • 操作として、ユーザーがホームページにアクセスする
  • 結果として、10本の映画のリストが表示されるはずである

Mirageを使用してこのテストを記述する方法を見てみましょう。

最初のテスト

この例では、構文にCypressを使用していますが、Mirageは設定した任意のJavaScriptテストハーネスと連携します。

Cypressを使用したアプリでMirageを動作させるには、Cypressのクイックスタートを参照してください。

Cypressが接続されていると仮定して、このテストを記述できます

// homepage-test.js
it("shows the movies", () => {
  cy.visit("/")

  cy.get("li.selected").should("have.length", 10)
})

アプリは実行されますが、/api/moviesへのHTTPリクエストを行うとエラーが発生し、テストが失敗します。ここでMirageを導入できます。

インポートしてbeforeEachで起動しましょう

// homepage-test.js
import { createServer } from "miragejs"

let server

beforeEach(() => {
  server = createServer()
})

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

it("shows the movies", function () {
  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

Mirageを起動すると、開発時と同様に、アプリのネットワークリクエストをインターセプトします。したがって、次にテストを実行すると、次のようなエラーが表示されるはずです。

Mirage: アプリが'/api/movies'へのGETリクエストを試みましたが、このリクエストを処理するルートが定義されていません。

これで、このルートをモックできます。

import { createServer, Model } from "miragejs"

let server

beforeEach(() => {
  server = createServer({
    models: {
      movie: Model,
    },

    routes() {
      this.namespace = "api"

      this.resource("movie")
    },
  })
})

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

it("shows the movies", function () {
  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

アプリは実行されますが、テストは失敗します。コンソールのログを見ると、Mirageが/api/moviesへのリクエストを処理しましたが、データなしで応答したことがわかります。

これは、Mirageのデータベースが空であるためです。

ガイドの前のセクションで学習したように、seedsメソッドを使用して、ファクトリとフィクスチャでMirageのデータベースにシードを適用できます。しかし、テストでは、Mirageの状態を設定する自然な場所が既にあります。それはテスト自体です。したがって、一般的なプラクティスはシードを使用せず、代わりに各テスト内でMirageのデータベース状態を設定することです。

server.createメソッドとserver.createListメソッドを、テストの本体内で直接使用してそれを行うことができます。

import { createServer, Model } from "miragejs"

let server

beforeEach(() => {
  server = createServer({
    models: {
      movie: Model,
    },

    routes() {
      this.namespace = "api"

      this.resource("movie")
    },
  })
})

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

it("shows the movies", function () {
  server.createList("movie", 10)

  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

これで、テストに合格しました!

各テスト後、Mirageのサーバーはシャットダウンされてリセットされるため、この状態がテスト間でリークすることはありません。

開発環境とテスト環境でサーバーを共有する

上記の例では、テスト内で直接新しいサーバーを設定しましたが、Mirageは、モックサーバーの定義が集中化され、開発環境とテスト環境で共有されている場合に最も効果的に使用できます。結局のところ、本番環境では、アプリは単一のAPIコントラクトを使用する実際のサーバーと通信します。したがって、単一のMirageサーバーを使用すると、使用されているすべての場所で一貫したモックAPIサーバーを維持するのに役立ちます。

したがって、開発用に開始したMirageサーバーがまだない場合は、サーバーの定義を、開発環境とテストの両方で使用されることが明確な場所に移動してください。

└── src
    ├── App.js
    ├── App.test.js
    └── mirage.js

次に、Mirageサーバーの起動に使用できる関数をエクスポートします。

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

export function startMirage() {
  return createServer({
    models: {
      movie: Model,
    },

    routes() {
      this.namespace = "api"

      this.resource("movie")
    },
  })
}

次に、この関数をインポートして、テストで使用します。

// App.test.js
import { startMirage } from "./mirage"

describe("homepage", function () {
  let server

  beforeEach(() => {
    server = startMirage()
  })

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

  it("shows the movies", function () {
    server.createList("movie", 10)

    cy.visit("/")

    cy.get("li.movie").should("have.length", 10)
  })
})

これで、Mirageサーバーを定義および更新する中心的な場所と、テストで簡単に使用する方法ができました。

startMirage関数を使用して、開発中にMirageを起動することもできます。

// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
import { startMirage } from "./mirage"

if (process.env.NODE_ENV === "development") {
  startMirage()
}

ReactDOM.render(<App />, document.getElementById("root"))

理想的には、Mirageコードが本番環境に含まれないようにする必要があります(プロトタイプを構築している場合を除きます)。これを実現する方法は、ビルドツールセットアップによって異なります。将来、これについて詳しく説明するガイドを追加します。

test環境

Mirageには、デフォルトでdevelopmentに設定されるenvironmentオプションがあります。開発環境では、Mirageの遅延は50msであり(カスタマイズ可能)、すべての応答をコンソールにログし、開発用のseedsを読み込みます。

Mirageはtest環境に入れることもできます。これにより、遅延が0(テストを高速に保つため)で開始され、すべてのログが抑制されます(CIサーバーのログを汚さないため)。また、seeds()関数も無視するため、データは開発専用に使用でき、テストにリークしたり影響を与えたりすることはありません。これは、テストを決定論的に保つのに役立ちます。

テストでtest環境を使用するには、startMirage関数を更新して、環境オプションを受け入れるようにしましょう。これはデフォルトでdevelopmentになります。

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

- export function startMirage() {
+ export function startMirage({ environment: 'development' }) {
    return createServer({
+     environment,

      models: {
        movie: Model,
      },

      routes() {
        this.namespace = "api"

        this.resource("movie")
      },
    })
  }

これで、テストでtestを環境として渡すことができます。テストは遅延なしで実行され、seeds()は実行されず、ログも表示されません。

  // src/App.test.js
  import { startMirage } from "./mirage"

  describe("homepage", function() {
    let server

    beforeEach(() => {
-     server = startMirage()
+     server = startMirage({ environment: 'test' })
    })

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

    it("shows the movies", function() {
      server.createList("movie", 10)

      cy.visit("/")

      cy.get("li.movie").should("have.length", 10)
    })
  })

テストをデバッグしていて、Mirageに出入りするネットワークリクエストを表示したい場合は、server.loggingオプションをtrueに設定することで、そのテスト内でログを有効にできます。

it("shows the movies", function () {
  server.logging = true // enable logs for this test while debugging
  server.createList("movie", 10)

  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

CIログをクリーンに保つために、完了したら削除してください。

テストの焦点を絞る

ファクトリは、テストに関連するコードをできるだけテストの近くに保つ上で重要です。上記の例では、サーバー上にそれらのムービーが存在する場合、ユーザーが10個のムービーを見ることを確認したかったのです。そのため、server.createList('movie', 10)の呼び出しは、テスト内で直接行われました。

ユーザーが「Interstellar」というタイトルのムービーの詳細ルートにアクセスしたときに、そのタイトルが<h1>タグに表示されることをテストしたいとします。これを実現する1つの方法は、サーバーのMovieファクトリにタイトルをハードコードすることです。

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

export function startMirage({ environment: 'development' }) {
  return createServer({
    environment,

    models: {
      movie: Model,
    },

    factories: {
      movie: Factory.extend({
        title: 'Interstellar'
      })
    },

    routes() {
      this.namespace = "api"

      this.resource("movie")
    },
  })
}

このアプローチの問題は、この1つのテストに非常に固有の共有Mirageサーバーに変更を加えたことです。

別のテストで、異なるタイトルのムービーについて異なることを検証する必要があるとします。そのケースに合わせてファクトリを変更すると、このテストが中断されます。

このため、モデルの特定の属性をオーバーライドするには、createcreateListを使用する必要があります。これにより、テストスイートの残りを脆弱にすることなく、テストに関連するコードをテストの近くに保つことができます。

// App.test.js
let server

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

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

it("shows all the movies", function () {
  server.createList("movie", 10)

  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

it("shows the movie's title on the detail route", function () {
  let movie = this.server.create("movie", {
    title: "Interstellar",
  })

  cy.visit(`/${movie.id}`)

  cy.get("h1").should("contain", "Interstallar")
})

これらの2つのテストでは、テスト内に関連するすべてのデータを作成します。

最初のテストでは、server.createList('movie', 10)を呼び出し、属性のオーバーライドを指定しません。これは、各ムービーの具体的な詳細がテストのアサーションに関連しないためです。

2番目のテストでは、特定のタイトルでserver.createを使用します。これは、テストがUIにタイトルが表示されることを検証しているためです。このテストでは、Mirageの自動生成IDを使用して、特定のムービーの動的なURLにアクセスしていることも確認できます。

複数のテスト間で共有されるデータ、または開発とテストの両方で使用されるデータを設定したい場合は、間違いなくあります。これについては、このページの下部でさらに説明します。

Arrange, Act, Assert

Mirageでは、テストを作成するために、Arrange, Act, Assertアプローチを使用することを推奨しています。このパターンは、AAAテスト(「トリプルAテスト」)と呼ばれることもあります。

上記のテストでこの構造を確認できます。

it("shows all the movies", function () {
  // ARRANGE
  server.createList("movie", 10)

  // ACT
  cy.visit("/")

  // ASSERT
  cy.get("li.movie").should("have.length", 10)
})

このルールを破ることが理にかなっている場合(たとえば、テストの最初または途中にいくつかのアサーションを追加する場合)もありますが、一般的にはこのパターンに従うように努める必要があります。

エラーのテスト

アプリがサーバーエラーにどのように応答するかをテストするには、テスト内で直接ルートハンドラーを上書きできます。

import { Response } from "miragejs"

it("shows an error if the save attempt fails", function () {
  server.post("/questions", () => {
    return new Response(500, {}, { errors: ["The database went on vacation"] })
  })

  cy.visit("/")
    .contains("New")
    .click()
    .get("input")
    .type("New question")
    .contains("Save")
    .click()

  cy.get("h2").should("contain", "The database went on vacation")
})

このルートハンドラーの定義は、このテストの期間のみ有効であるため、終了するとすぐに、mirage.jsファイルで/questionsへのPOSTに対して定義したハンドラーが再度使用されます。

テストにおける共有データシード

Mirageの環境がtestに設定されている場合、seeds()設定オプションは無視されるため、変更してもテストスイートの残りに影響を与えることはありません。

開発シナリオとテストの間、または複数のテスト間で共有したいロジックがある場合は、常に新しいプレーンなJavaScriptモジュールを作成し、必要な場所にインポートできます。

開発中に共有モジュールを使用するには、モジュールを作成します。

// mirage/scenarios/shared.js
export default function(server) {
  server.loadFixtures('countries');

  server.createList('event', 10);
});

...seeds()でロードします。これにより、シナリオが開発中に実行されます。

// mirage.js
import sharedScenario from "./scenarios/shared"

createServer({
  seeds(server) {
    // Load the shared scenario in development
    sharedScenario(server)

    // Make some development-specific data
    server.create("movie", { title: "Interstellar" })
  },
})

...そして、テスト(または一般的なテスト設定関数)でもロードします。

import sharedScenario from "./mirage/scenarios/shared"
import { startServer } from "./mirage"

let server

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

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

it("shows all the movies", function () {
  server.createList("movie", 10)

  cy.visit("/")

  cy.get("li.movie").should("have.length", 10)
})

以上が、Mirageを使用したアプリケーションテストの基本です。次に、統合テストとユニットテストについて説明します。