React コンポーネントのテストを爆速で書くために個人的に従っているパターンを紹介します。サンプルでは Jest と Enzyme を使っていますが、論旨はこれらのツールに依存しないものになっているはずです。
この記事では、題材として画像のような2ステップのログイン画面をテストすることを考えます1。
このコンポーネントはユーザ名、パスワード、今いるステップ (1ステップ目か2ステップ目か) を状態として持っているものとします。2ステップ目でユーザがログインボタンを押すと、ユーザ名とパスワードを引数にしてコールバックを呼び出します。コンポーネントのシグニチャは下記のようなものだとしましょう。
export default function LoginForm({ onLoginButtonClick }) {/*...*/}
ファクトリ関数にすべてを押し込める
テストを書き始めるにあたり、コンポーネントのマウント、モック関数の作成など、諸々のセットアップを担当するファクトリ関数を作成します。ユーザインタラクションをシミュレートするためのあらゆる面倒をこの関数に押し付けます。
import React from "react";
import { mount } from "enzyme";
import LoginForm from "./LoginForm";
function createTestEnv() {
// モックの作成
const onLoginButtonClick = jest.fn();
// コンポーネントのマウント
const wrapper = mount(<LoginForm onLoginButtonClick={onLoginButtonClick} />);
// ユーザインタラクションをシミュレートする便利関数
function prompt() {
return wrapper.text();
}
function enterUsername(username) {
wrapper
.find('input[name="username"]')
.simulate("change", { target: { value: username } });
}
function enterPassword(password) {
wrapper
.find('input[name="password"]')
.simulate("change", { target: { value: password } });
}
function clickNext() {
wrapper.find('[children*="NEXT"]').simulate("click");
}
function clickLogin() {
wrapper.find('[children*="LOGIN"]').simulate("click");
}
return {
onLoginButtonClick,
wrapper,
prompt,
enterUsername,
enterPassword,
clickNext,
clickLogin
};
}
テスト本体からはファクトリ関数を呼び出すだけ
上で定義したファクトリ関数を各テストの冒頭で呼び出します。簡単!
describe("LoginForm", () => {
describe("the first step", () => {
it("prompts for a username", () => {
const { prompt } = createTestEnv();
expect(prompt()).toContain("Enter your username");
});
});
describe("the second step", () => {
it("prompts for a password", () => {
const { enterUsername, clickNext, prompt } = createTestEnv();
enterUsername("alice");
clickNext();
expect(prompt()).toContain("Enter your password");
});
});
describe("the onLoginButtonClick callback", () => {
it("gets called with username and password", () => {
const {
enterUsername,
enterPassword,
clickNext,
clickLogin,
onLoginButtonClick
} = createTestEnv();
enterUsername("alice");
clickNext();
enterPassword("pAsSwOrD");
clickLogin();
expect(onLoginButtonClick).toBeCalledWith({
username: "alice",
password: "pAsSwOrD"
});
});
});
});
何がうれしいのか
テスト本体の可読性が高まる
この書き方で気に入っているのは、面倒ごとをファクトリ関数に押し込むことでテスト本体の可読性が高まることです。例の中で最も長いテストでさえ自然言語として読むことができるくらいです。
enterUsername("alice");
clickNext();
enterPassword("pAsSwOrD");
clickLogin();
expect(onLoginButtonClick).toBeCalledWith({
username: "alice",
password: "pAsSwOrD"
});
実装依存が一か所にまとまる
ファクトリ関数に面倒を押し付けることの二つ目のメリットは、実装依存のコードが一か所に集まることです。UIのテストたるもの、常に「ユーザーから見えるものをテストする」のが理想ですが、現実にはそれが難しい場面も存在します。たとえば「フロッピーディスクのアイコンをクリックする」という操作をテストコードに落とし込むと、
wrapper.find('img[src="./icons/floppy-disk-icon.png"]').click();
のようにせざるを得ません。しかしここに登場する「フロッピーディスクアイコンのファイル名」は明らかに実装の詳細です。こうしたコードがテスト本体に何度も現れると、アイコンのファイル形式を png から svg に差し替えるといった軽微な変更を行うだけで、テストを何か所も変更する羽目になります。辛いですね。
一方で、実装依存のコードをファクトリ関数の中に押し込めておけば、ファクトリ関数の中の一か所を書き換えるだけでよくなります。実装依存のコードを一か所にかき集めることで、実装の変更の影響範囲を最小化できるのです。もちろん理想は実装依存が少ないテストコードです。
おわりに
私はここ最近、専ら本記事で紹介したスタイルで React コンポーネントのテストを書いています。悩む時間を減らして生産効率を上げるために、このような決まりきったパターンに従うことは有効だと感じています。スニペットを活用すればコーディングをさらに高速化できます。
このスタイルに起因する大きな問題は今のところ発生していません。しかし類例をあまり見かけないので、こうしたスタイルには問題があるものの、私がそれに気づいていないだけかもしれません。もしそのように思われる場合は、ぜひコメント欄でお知らせください。