こんにちは。
ギークフィードでエンジニアをやっているNkawaKです。
バックエンドのテストコードは書いたことはあっても、
フロントエンドのテストコードは経験がない方も少なくないのではないでしょうか。
本記事ではVitestというモダンなテストフレームワークを使って、
フロントエンドのテスト環境の構築やテストコードの書き方を解説していきますので、
これからフロントエンドのテストに入門する方の参考にしていただければと思います。
テスト環境の構築
まずはViteを使ってReactのプロジェクトを作成していきましょう。
1 |
yarn create vite react-test |
1 |
Select a framework: › React |
1 |
Select a variant: › TypeScript + SWC |
これでReactのプロジェクトが作成できました。
これからの作業はこのディレクトリで行っていきます。
今回は↓のような、よくあるログインフォームをサンプルにテストしてみましょう。
このフォームは以下のコンポーネントからできています。
メールアドレスとパスワードをuseStateで管理し、そのデータをログインボタンのイベントでAPIに送信します。
API側ではメールアドレスとパスワードでユーザーを検索して、ユーザーが見つかったらレスポンスとして返します。
以上の処理が正常に完了したら、取得したデータのユーザー名をフォーム上に表示します。
次はこのコンポーネントをテストするための環境を構築していきましょう。
以下のコマンドを実行してテストに必要なライブラリをインストールします。
1 |
yarn add -D jsdom vitest @vitest/coverage-v8 @vitest/ui |
1 |
yarn add -D @testing-library/jest-dom @testing-library/react @testing-library/user-event |
ライブラリのインストールが完了したら、Vitestの設定をしていきましょう。
vite.config.tsを以下のように編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// Vitestの型を追加する /// <reference types="vitest" /> /// <reference types="vite/client" /> import { defineConfig } from "vite"; import { configDefaults } from "vitest/config"; import react from "@vitejs/plugin-react-swc"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], test: { // テストに関するAPIをグローバルに設定 globals: true, // テスト環境の設定 environment: "jsdom", // テストの設定ファイル setupFiles: "./src/test/setup.ts", // CSSファイルを処理する css: true, // テストのカバレッジを出力する設定 coverage: { // @vitest/coverage-v8を設定 provider: "v8", exclude: [ ...(configDefaults.coverage.exclude as string[]), "src/test", "src/main.tsx", ], }, }, }); |
次にpackage.jsonにテスト用のスクリプトを追加します。
1 2 3 4 5 6 7 8 9 |
"scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "test": "vitest", "test:ui": "vitest --ui", "coverage": "vitest run --coverage" }, |
次にテストの設定ファイルを配置するディレクトリを作成し、そこにsetup.tsを配置します。
1 |
mkdir src/test |
1 |
import '@testing-library/jest-dom'; |
この時点ではsetup.tsに、importを一行追加するだけで大丈夫です。
最後にtsconfig.jsonでVitestでよく使うAPIの型を、あらかじめimportしておきます。
1 2 3 4 5 6 7 8 9 10 |
{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "types": ["vitest/globals"], ... |
以上でVitestのテスト環境を構築することができました。
では実際にテストコードを書いていきましょう。
フロントエンドのテストコードを書く
src/testにApp.test.tsxを新規作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import App from "../App"; import { render, screen } from "@testing-library/react"; describe(App, () => { test("ログインフォームが表示されている", async () => { render(<App />); expect(screen.getByText("ログイン")).toBeInTheDocument(); expect(screen.getByText("メールアドレス")).toBeInTheDocument(); expect(screen.getByText("パスワード")).toBeInTheDocument(); expect(screen.getByText("ログインする")).toBeInTheDocument(); expect(screen.getByRole("textbox")).toBeTruthy(); expect(screen.getByRole("button")).toBeTruthy(); expect(screen.getByLabelText("メールアドレス")).toBeTruthy(); expect(screen.getByLabelText("パスワード")).toBeTruthy(); }); }); |
まずはログインフォームが正常に表示されているかのテストをしていきましょう。
ファイルのトップレベルでdescribeというAPIを実行します。
describeは文字列か関数を第一引数に指定し、第二引数のコールバック関数の中にテストケースを書いていきます。
第一引数の文字列か関数名が、describeの中に記述する複数のテストをまとめるグループ名となります。
次にtestというAPIを実行して、テストを定義します。
テストケースを書いていく前に、renderでテストをするコンポーネントをレンダリングする必要があります。
getByTextで引数に指定された文字列が含まれる要素を取得し、
1 |
expect(screen.getByText("ログイン")).toBeInTheDocument(); |
toBeInTheDocumentでその要素がドキュメントの中に存在しているかを確認しています。
getByRoleでは引数で指定したロールを持つ、要素を取得することができます。
1 |
expect(screen.getByRole("textbox")).toBeTruthy(); |
このロールはAccessible Rich Internet Applications (ARIA)を参照しています。
https://momdo.github.io/html-aria/#allowed-descendants-of-aria-roles
getByLabelTextは引数の文字列を含むラベルに紐づいた要素を取得します。
1 |
expect(screen.getByLabelText("パスワード")).toBeTruthy(); |
type=”password”のinputはgetByRoleでは取得できないので、このメソッドを使用しています。
これら以外にも、getByTestIdというメソッドが頻出です。
このメソッドではid属性ではなく、data-testidという属性から要素を取得することに注意が必要です。
getByRoleとgetByLabelTextで取得した要素が存在しているかをtoBeTruthyで確認しています。
1 |
expect(screen.getByRole("textbox")).toBeTruthy(); |
要素が取得できなかった場合は結果はfalsyとなるので、期待する要素が存在するかを確認できます。
ではこのテストコードをyarn testで実行してみましょう。
テストが成功したことを確認できました。
次は入力フォームが動作しているかのテストをしていきましょう。
以下のテストを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import App from "../App"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; // 追加 describe(App, () => { ... test("メールアドレスが入力できる", async () => { render(<App />); const user = userEvent.setup(); const mailForm = screen.getByLabelText("メールアドレス"); await user.type(mailForm, "test@example.com"); expect(mailForm).toHaveValue("test@example.com"); }); test("パスワードが入力できる", async () => { render(<App />); const user = userEvent.setup(); const passwordForm = screen.getByLabelText("パスワード"); await user.type(passwordForm, "userTest1234"); expect(passwordForm).toHaveValue("userTest1234"); }); }); |
テストコード上でフォームの入力やクリックなどのイベントを発生させる場合は、userEventを使用します。
イベントを起こす前にsetupで準備する必要があります。
先ほど紹介したgetByLabelTextでフォームの要素を取得します。
取得した要素に文字を入力する場合はtypeを実行します。
typeの第一引数には入力を行う要素を、第二引数には入力する文字列を指定します。
またtypeは非同期で実行されるのでawaitで処理の完了を待機しています。
フォームに入力された値をtoHaveValueで確認します。
新しく追加したテストも成功しました。
以上のテスト結果から、期待したテキストが画面に含まれていることと
フォームが正常に動作していることが分かりました。
次にAPIと連携している部分をテストします。
APIをモック化する
最後にログイン処理をテストしましょう。
1 2 3 4 5 6 7 8 |
const submit = async () => { const response = await fetch("https://XXXXXXX/signin", { method: "POST", body: JSON.stringify({ mail, password }), }); const body = await response.json(); body && setUserData(body); }; |
この処理はAPIとやりとりを行いますが、テスト環境では外部のAPIと連携することができません。
ではどうやってテストするかというと、APIの部分をモック化することで解決します。
APIのモック化はmswというライブラリを使います。
1 |
yarn add -D msw |
ライブラリをインストールしたらsrc/test/にmocksというディレクトリを作成します。
1 |
mkdir src/test/mocks |
このディレクトリでhandlers.tsというファイルを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { HttpResponse, http } from "msw"; export const handlers = [ http.post("https://XXXXXXX/signin", async ({ request }) => { const { mail, password } = (await request.json()) as { mail?: string; password?: string; }; if (mail === "test@example.com" && password === "userTest1234") { return HttpResponse.json( { id: "1", name: "testUser", mail: "test@example.com", }, { status: 200 } ); } else { return HttpResponse.json(null, { status: 404 }); } }), ]; |
handlersの配列の中でモックAPIの動作を定義します。
テスト環境では”https://XXXXXXX/signin”に”test@example.com”と”userTest1234″がPOSTされた場合に、
ステータスコード200とユーザーデータのオブジェクトを返し、それ以外では404エラーを返すようにしました。
次はsrc/test/にserver.tsというファイルを作成します。
1 2 3 4 |
import { setupServer } from "msw/node"; import { handlers } from "./handlers"; export const APIServer = setupServer(...handlers); |
setupServerに先ほど作成したhandlersの配列を渡すことで、サーバーとして機能してくれます。
最後にsrc/test/setup.tsを以下のように編集します。
1 2 3 4 5 6 7 |
import "@testing-library/jest-dom"; import { APIServer } from "./mocks/server"; beforeAll(() => APIServer.listen()); afterAll(() => APIServer.close()); afterEach(() => APIServer.resetHandlers()); |
setupServerで作成したサーバーをテストする際に、どのように動作させるかを定義しています。
beforeAllはdescribe内の最初のテストが始まる前に実行するコールバック関数を登録し、
テスト前にサーバーを待機状態にしています。
afterAllはdescribe内の最後のテストが終了した際に実行する関数を登録し、
テスト後にサーバーを終了させています。
afterEachはdescribe内の各テストが終わる毎に実行する関数を登録し、
テスト毎にリクエストハンドラをリセットしています。
モックAPIの準備ができたので、App.test.tsxに以下のテストコードを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
describe(App, () => { ... test("ログインができる", async () => { render(); const user = userEvent.setup(); const mailForm = screen.getByLabelText("メールアドレス"); const passwordForm = screen.getByLabelText("パスワード"); const sendButton = screen.getByRole("button"); await user.type(mailForm, "test@example.com"); await user.type(passwordForm, "userTest1234"); await user.click(sendButton); expect(screen.getByText("Welcome testUser !")).toBeInTheDocument(); }); }); |
setupでイベントの準備を行い、getByで要素を取得します。
typeからフォームにメールアドレスとパスワードを入力して、
clickでログインボタンをクリックしてAPIにフォームの情報を送信します。
これらの処理が正常に実行できていれば、画面上に”Welcome testUser !”と表示されるはずです。
テストを実行して確認してみましょう。
最後に追加したテストも成功しました。モック化したAPIも正常に動作しています。
すべてのテストが成功したことで、このコンポーネントは期待通りに動作していることが分かりました。
Vitest UIとカバレッジレポート
テスト環境を構築する際にpackage.jsonに以下のスクリプトも追加していました。
1 2 |
"test:ui": "vitest --ui", "coverage": "vitest run --coverage" |
これらのVitestのオプションも見ておきましょう。
yarn test:uiを実行するとテスト結果をUIで確認することができます。
テストの結果をダッシュボードとして出力したり、左のファイル名をクリックすると、
各テストのより詳しい情報を確認することができます。
実際のアプリではもっと多くのテストコードが書かれるので、便利なUIがあるのはありがたいですね。
yarn coverageを実行するとテストのカバレッジ(網羅率)が表示されます。
このレポートを見れば、どのコンポーネントがテストできていないのか一目でわかりますね。
またカバレッジテストの後、/coverageにレポートのHTMLページが作成されます。
このディレクトリの中にあるindex.htmlを開くと、ブラウザ上でより詳しくレポートの確認ができます。
File列のリンクから、各コンポーネントのテスト状況を個別に確認することもできます。
以上のように、Vitestには便利なオプションがあるので、テストの際に活用してみるのも良いでしょう。
まとめ
- Vitestのテスト環境を構築する際はvite.config.tsにテストの設定を追加する
- テストコードのトップレベルでdescribeを実行し、その中でテストを定義する
- getByから始まるメソッドで要素を取得し、expectのメソッドで取得した要素の確認を行うことができる
- 入力やクリックなどのイベントはuserEventで発生させることができる
- mswというライブラリを使って、APIをモック化することができる
- Vitestにはテスト結果をUIやカバレッジレポートとしても出力させることができる
Vitestによるフロントエンドのテスト解説は以上になります。
皆さまがテストコードを書くときの参考になれば幸いです。
- 【React】フロントエンドのテストコードを書いてみよう【Vitest】 - 2024-04-30
- React Hooks入門ガイド - 2022-03-07
- Vue.jsで構成するシングルページアプリケーション(SPA)の作り方やサンプル例【後編】 - 2020-02-07
- Vue.jsで構成するシングルページアプリケーション(SPA)の作り方やサンプル例【前編】 - 2019-08-05