パート9 – テスト

この最終セクションでは、さまざまなサーバー状態を考慮して、Mirage を使用してアプリケーションをテストする方法を学習します。

プロジェクトは既にJestTesting Libraryを使用して設定されています。また、指定されたURLでリマインダーアプリをレンダリングするvisit(url)ヘルパーも提供しています。

src/__tests__/app.jsを開いて、最初のテストを書いてみましょう。

リマインダーがない場合、アプリに「完了!」と表示されることを確認したいと考えています。テストのコードを次に示します。プロジェクトにコピーしてください。

// __tests__/app.js
import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"

test("it shows a message when there are no reminders", async () => {
  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("All done!")).toBeInTheDocument()
})

新しいターミナルウィンドウでyarn testを実行します。Jestはウォッチャーを開始し、変更を行うたびにテストを再実行します。

テストが最初のランを完了した後、エラーが表示されます。

テキスト「完了!」を持つ要素が見つかりません

「ネットワークリクエストが失敗しました」というエラーも表示されます。

デバッグ出力を見ると、このチュートリアルのパート1で表示された、おなじみのネットワークエラーUIがDOMに表示されていることがわかります。

Network error

パート1と同様に、これはアプリが/api/remindersへの最初のフェッチを実行しているが、それに応答するサーバーがないため発生しています。テストにMirageサーバーを導入する時です。

makeServer関数をインポートし、テストの先頭で実行しましょう。

import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"

test("it shows a message when there are no reminders", async () => {
  makeServer()
  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("All done!")).toBeInTheDocument()
})

テストはまだ失敗していますが、デバッグ出力をスクロールすると、ネットワークリクエストの失敗に関するメッセージは表示されなくなっています。

代わりに、チュートリアルの前のパートで作成した既存のリマインダーがDOMにレンダリングされていることがわかります。seeds()フックから。

Dev seeds

これは、開発時と同じMirageサーバーを作成しており、アプリの開発に役立つこれらの5つのリマインダーでサーバーにシードデータを設定しているため、理にかなっています。

そのため、データのない、新しいMirageサーバーインスタンスを用意して、「完了!」メッセージが空の状態に表示されることを確認したいと考えています。これを実現する1つの方法は、サーバー定義に戻り、すべてのseeds()データを削除することです。

しかし、開発用シードを変更する必要がない、より良い方法があります。environmentオプションを使用して、「テスト」モードでMirageサーバーを起動できます。

これがどのように機能するかを確認するには、server.jsファイルに戻り、environment: 'test'オプションを追加します。

// server.js
import { createServer } from "miragejs"

export default function () {
  return createServer({
    environment: "test",

    // rest of server
  })
}

その変更を保存すると、テストが再実行され、合格するはずです!

では、「test」環境はサーバーに何をしているのでしょうか?いくつかの点があります。timingを0に設定してテストを高速化します。Mirageのログを非表示にして、テスト出力がクリーンな状態を保ちます。そして最も重要なのは、seeds()フックをスキップすることです。

そのため、すべてのモデル、シリアライザ、ファクトリ、ルートを再利用できますが、seeds()データセットは開発モード用に分けておくことができます。テストでは、各テストを使用して、そのテストに必要な正確な状態でサーバーのデータを設定します。

現在のテストでは、データベースを空にしたいので、追加のデータを作成せずにサーバーを起動するだけです。これにより、MirageがGET APIリクエストを正しく処理し、空のデータセットで応答します。「完了!」メッセージのアサーションが合格するのは、まさにこのテストで求めているものです。

First test passing

開発サーバーに戻ると(または別のターミナルウィンドウでyarn startを実行すると)、seeds()で作成したリマインダーが表示されなくなります。これは、開発サーバーもテストモードで実行されているためです。

server.jsからエクスポートしている関数を更新して、環境引数を受け取るようにし、それを使用してサーバーの環境を設定しましょう。

export default function (environment = "development") {
  return createServer({
    environment,

    // ...rest of server
  })
}

これで、開発サーバーは再びシードデータを使用し、開発に役立つ人工的な遅延とコンソールログがありますが、テストは再び失敗するはずです。

テストファイルに戻り、makeServerへの呼び出しを更新し、「test」環境を渡します。

import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"

test("it shows a message when there are no reminders", async () => {
  makeServer("test")
  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("All done!")).toBeInTheDocument()
})

これでテストは合格しますが、開発環境には開発中に役立つ独自の分離されたシードデータがあります!

これはMirageを設定する1つの方法にすぎません。独自のプロジェクトでは、引数の設定方法とデフォルトの選択方法はユーザー次第です。しかし、重要なのは、開発環境とテスト環境の両方でMirageサーバーを共有する必要があるということです。

テストで簡単に分離されたMirageサーバーを作成できるようになったので、サーバー上に既に3つのリマインダーが存在する場合のUIのテスト方法を見ていきましょう。

前のテストをコピーして貼り付け、説明を更新することから始めます。

test("it shows existing reminders", async () => {
  makeServer("test")
  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("All done!")).toBeInTheDocument()
})

保存するとすぐに、Jestは両方のテストを実行し、次の警告が表示されます。

既に1つ実行中のPretenderインスタンスを作成しました。

Mirageは内部でPretenderライブラリを使用しており、Pretenderから互いに衝突する2つのサーバーがあるという警告が出ています。以前のテストの後処理と、この新しいテストの終了後にも、server.shutdown()メソッドを使用してクリーンアップする必要があります。

server.jsファイルを開き、makeServer関数がサーバーインスタンスを返すようにします。

// server.js
export default function (environment = "development") {
  return createServer({
    // rest of server
  })
}

そして、テストに戻って、makeServerの戻り値をローカル変数に代入し、それを用いて各テストの最後にserver.shutdown()を呼び出します。

test("it shows a message when there are no reminders", async () => {
  let server = makeServer("test")

  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("All done!")).toBeInTheDocument()
  server.shutdown()
})

test("it shows existing reminders", async () => {
  let server = makeServer("test")

  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("All done!")).toBeInTheDocument()
  server.shutdown()
})

これで、両方のテストが実行され、Pretenderからの警告は表示されなくなります。

では、実際に2番目のテストを書いてみましょう。

サーバーが3つのリマインダーで開始される場合のUIの動作をテストしたいので、visit('/')を呼び出す前に、そのデータを作成する必要があります。

新しいローカル変数serverを使用して、seeds()フック内で行うのと全く同じ方法で、テスト内で直接サーバーにシードできます。server.createステートメントを3つコピーして、テストに取り込みましょう。

test("it shows existing reminders", async () => {
  let server = makeServer("test")
  server.create("reminder", { text: "Walk the dog" })
  server.create("reminder", { text: "Take out the trash" })
  server.create("reminder", { text: "Work out" })

  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("All done!")).toBeInTheDocument()
})

テスト失敗が表示されるはずです。

テキスト「完了!」を持つ要素が見つかりません

デバッグ出力を見ると、HTMLに3つのリマインダーが表示されているはずです。

Three reminders debug

まさに私たちが望んでいたとおりです!

アサーションを更新しましょう。

test("it shows existing reminders", async () => {
  let server = makeServer("test")
  server.create("reminder", { text: "Walk the dog" })
  server.create("reminder", { text: "Take out the trash" })
  server.create("reminder", { text: "Work out" })

  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("Walk the dog")).toBeInTheDocument()
  expect(screen.getByText("Take out the trash")).toBeInTheDocument()
  expect(screen.getByText("Work out")).toBeInTheDocument()
  server.shutdown()
})

これで、両方のテストがパスするようになりました!

ご覧のように、各テストは、テストするシナリオのためにMirageサーバーの状態を変更するための独立した場所を提供します。各テストの後でクリーンアップを行うため、これらの変更はテスト間で漏れません。

簡単なリファクタリングを行いましょう。各テストで基本的なMirageサーバーの起動と停止を行うため、JestのbeforeEachafterEachフックを使用してこれをクリーンアップできます。

import { visit } from "../lib/test-helpers"
import { screen, waitForElementToBeRemoved } from "@testing-library/react"
import makeServer from "../server"

let server

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

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

test("it shows a message when there are no reminders", async () => {
  visit("/")

  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("All done!")).toBeInTheDocument()
})

test("it shows existing reminders", async () => {
  server.create("reminder", { text: "Walk the dog" })
  server.create("reminder", { text: "Take out the trash" })
  server.create("reminder", { text: "Work out" })

  visit("/")
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  expect(screen.getByText("Walk the dog")).toBeInTheDocument()
  expect(screen.getByText("Take out the trash")).toBeInTheDocument()
  expect(screen.getByText("Work out")).toBeInTheDocument()
})

保存すると、両方のテストが再びパスするはずです。

この変更により、サーバーは常にクリーンアップされ、現実世界のユーザーストーリーに関連するより高度な手順に焦点を当てたテストを作成できます。例:「サーバー上に3つのリマインダーが存在する場合、ユーザーがアプリにアクセスすると、ページ上にそれらが表示されることを期待する」というテストです。

もう1つのテストを作成しましょう。特定のリストに新しいリマインダーを作成できることをテストします。

リストをMirageサーバーにシードしてから、そのリストのURLにアクセスすることでテストを開始します。

test("it can add a reminder to a list", async () => {
  let list = server.create("list")

  visit(`/${list.id}`)
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))
})

テスト内でMirageのデータを使用することがいかに便利であるかに注目してください。この時点でのレンダリングされた出力を確認したい場合は、?openクエリパラメーターを使用してアプリのサイドバーが開いていることを確認し、screen.debug()を呼び出して出力にリストを表示できます。

test("it can add a reminder to a list", async () => {
  let list = server.create("list")

  visit(`/${list.id}?open`)
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  screen.debug()
})

サイドバーUIに「すべて」と「リスト0」が表示されるはずです。

New list in Add Reminders test

このURLから新しいリマインダーを作成すると、パート7で開発中にこれを行ったときと同様に、このリストに関連付けられます。

手順を見ていきましょう。まず、ファイルの先頭にuserEventのインポートを追加します。

import userEvent from "@testing-library/user-event"

次に、それを使用して適切な要素をクリックして入力します。data-testid属性を使用してそれらを識別しています。

test("it can add a reminder to a list", async () => {
  let list = server.create("list")

  visit(`/${list.id}`)
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  userEvent.click(screen.getByTestId("add-reminder"))
  await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
  userEvent.click(screen.getByTestId("save-new-reminder"))

  // assert something
})

送信ボタンをクリックした後は、どうすればよいでしょうか?

開発に戻ってリマインダーを作成しようとすると、リマインダーが作成されるとテキストボックスがフェードアウトして非表示になります。

したがって、テストでは、入力が消えるのを待ってから、新しいリマインダーがリストに表示されることをアサートできます。

test("it can add a reminder to a list", async () => {
  let list = server.create("list")

  visit(`/${list.id}`)
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  userEvent.click(screen.getByTestId("add-reminder"))
  await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
  userEvent.click(screen.getByTestId("save-new-reminder"))

  await waitForElementToBeRemoved(() => screen.getByTestId("new-reminder-text"))

  expect(screen.getByText("Work out")).toBeInTheDocument()
})

そして、それは動作します!

最後のステップとして、フロントエンドコードが想定どおりに動作しているという確信を得るために、Mirageサーバーの状態をアサートすることが理にかなっている場合があります。

私たちのケースでは、すべてが正しく動作していれば、Mirageのデータベースに新しいリマインダーがあり、作成したリストに関連付けられているはずです。これらのアサーションは、UIアサーションと並んで簡単に追加できます。

test("it can add a reminder to a list", async () => {
  let list = server.create("list")

  visit(`/${list.id}`)
  await waitForElementToBeRemoved(() => screen.getByText("Loading..."))

  userEvent.click(screen.getByTestId("add-reminder"))
  await userEvent.type(screen.getByTestId("new-reminder-text"), "Work out")
  userEvent.click(screen.getByTestId("save-new-reminder"))

  await waitForElementToBeRemoved(() => screen.getByTestId("new-reminder-text"))

  expect(screen.getByText("Work out")).toBeInTheDocument()
  expect(server.db.reminders.length).toEqual(1)
  expect(server.db.reminders[0].listId).toEqual(list.id)
})

これは、HTTPリクエストとレスポンスデータに対してアサートする低レベルに落とすことなく、UIが正しいJSONペイロードをワイヤー経由で送信していることを検証する簡単な方法と考えてください。


うーむ、これはチュートリアルの最も長いステップでしたが、多くのことを達成しました!アプリの重要な機能を網羅する4つのテストを作成し、テストごとに必要な変更のみを行うことで、既存のMirageサーバーを再利用することができました。

既存のMirageサーバーを活用し、テストごとにサーバーを特定の状態にするために必要な変更のみを行うことで、テストの作成がどれだけ快適になるかご理解いただけたと思います。

要点

  • Mirageを使用すると、開発とテストの間でモックサーバーを簡単に共有できます。
  • テストを行う場合は、Mirageサーバーにtest環境を使用してください。これにより、テストは高速に実行され、データベースは空の状態から開始されます。
  • テスト内でMirageサーバーに簡単にアクセスできることを利用して、動的なURLにアクセスしたり、サーバーのデータベースに加えられた変更をアサートしたりすることができます。