はじめに
こんにちは!開発チームの遠藤です。
MRC(Markerise Cloud)では、現在フロントエンドのリプレイスが行われており、
テストコードを書きながら開発しています。
本記事では、実際に React コンポーネントをどのような観点で、
テストコードを書いているのかを、簡単にですが、紹介したいと思います。
環境
- react: v17.0.2
- react-dom: v17.0.1
- @testing-library/react: v11.1.2
- @testing-library/react-hooks: v3.4.2
テスト観点
テストの観点や粒度は、プロジェクトによって様々ですが、
フロントエンド開発では、以下の種別でテストコードを書いています。
- コンポーネント
- カスタムフック
- 関数
何をチェックしているのか
- コンポーネントテスト
- DOM を参照して見た目をチェック
- コンポーネントがレンダリングされたとき
- ユーザーイベントが発火されたとき e.g.
- DOM を参照して見た目をチェック
- カスタムフックテスト
- 状態と更新関数の紐づきをチェック
- 状態と更新関数の紐づきをチェック
- 関数テスト
- 関数内の処理をチェック
コード例 (コンポーネント)
実際にコンポーネントの例を書いてみました。(あくまでも例なのでご了承ください)
機能としては、ファイル選択を行うと選択されたファイルのサイズを byte ~ TB の単位付きで表示するものです。
コンポーネント
type Testable = { "data-testid"?: string | undefined };
export const FileSizeCheck: VFC<Testable> = ({ "data-testid": testid }) => {
const [file, setFile] = useInputFileState();
const fileSize = useMemo(
() => file && convertFileSize(file.size).join(""),
[file]
);
return (
<div data-testid={testid}>
<input type="file" onChange={setFile} />
{fileSize ? <div>{`Size: ${fileSize}`}</div> : <></>}
</div>
);
};
テスト種別に当てはめてみる
上のコードをテスト種別に当てはめてみると、次のようになります。
- コンポーネント →
- カスタムフック → useInputFileStateが対象
- 関数 → convertFileSizeが対象
コード例 (カスタムフック・関数)
以下、コンポーネント内で使用しているコードになります。
カスタムフック
export const useInputFileState = (): [
File | undefined,
Dispatch<ChangeEvent<HTMLInputElement>>
] => {
const [value, setValue] = useState<File>();
return [value, getInputOnChange(setValue)];
};
const getInputOnChange =
(setValue: Dispatch<File>) => (event: ChangeEvent<HTMLInputElement>) => {
if (!event.target.files?.length) return;
const file = event.target.files[0];
setValue(file);
};
関数
type FileSizeUnit = "TB" | "GB" | "MB" | "KB" | "byte";
export const convertFileSize = (size: number): [number, FileSizeUnit] => {
if (size >= 1024 ** 4) return [calc(size, 4), "TB"];
if (size >= 1024 ** 3) return [calc(size, 3), "GB"];
if (size >= 1024 ** 2) return [calc(size, 2), "MB"];
if (size >= 1024 ** 1) return [calc(size, 1), "KB"];
return [size, "byte"];
};
const calc = (size: number, n: number) =>
Math.floor((size / 1024 ** n) * 100) / 100;
テストコードを書いてみる
次にテストコードを書いてみます。
実際には、仕様に沿ってきちんとテスト項目の洗い出しに加え、同値分割・境界値分析等を用いて、
適切なテストデータを作成する必要がありますが、割愛させて頂きます。
コンポーネントテスト
const fileMock = new File(["test"], "test.csv");
describe("FileSizeCheck", () => {
test("ファイルを選択するとファイルのサイズが表示される", () => {
const { getByTestId, queryByText } = render(
<FileSizeCheck data-testid="file-size-check" />
);
const component = getByTestId("file-size-check");
expect(queryByText("Size: 4byte")).toBeFalsy();
const input = component.getElementsByTagName("input")[0];
fireEvent.change(input, { target: { files: [fileMock] } });
const file = input.files && input.files[0];
expect(file).toBeTruthy();
expect(file?.name).toBe("test.csv");
expect(queryByText("Size: 4byte")).toBeTruthy();
});
});
カスタムフックテスト
const changeEventFileMock = {
target: { files: [fileMock] },
} as unknown as ChangeEvent<HTMLInputElement>;
const changeEventEmptyFileMock = {
target: { files: [] },
} as unknown as ChangeEvent<HTMLInputElement>;
describe("useInputFileState", () => {
test("stateの初期値はundefinedである", () => {
const { result } = renderHook(() => useInputFileState());
const [value] = result.current;
expect(value).toBeUndefined();
});
test("changeイベント(Fileあり)を受け取るとstateにFileがセットされる", () => {
const { result } = renderHook(() => useInputFileState());
const [, setValue] = result.current;
act(() => setValue(changeEventFileMock));
const [value] = result.current;
expect(value).toBe(fileMock);
});
test("changeイベント(Fileなし)を受け取ってもstateは変化しない", () => {
const { result } = renderHook(() => useInputFileState());
const [, setValue] = result.current;
act(() => setValue(changeEventFileMock))
act(() => setValue(changeEventEmptyFileMock));
const [value] = result.current;
expect(value).toBe(fileMock);
});
});
関数テスト
describe("convertFileSize", () => {
test(`引数 1024 ** 4 の戻り値は [1, "TB"] である`, () => {
const result = convertFileSize(1024 ** 4);
expect(result).toEqual([1, "TB"]);
});
test(`引数 1024 ** 3 の戻り値は [1, "GB"] である`, () => {
const result = convertFileSize(1024 ** 3);
expect(result).toEqual([1, "GB"]);
});
test(`引数 1024 ** 2 の戻り値は [1, "MB"] である`, () => {
const result = convertFileSize(1024 ** 2);
expect(result).toEqual([1, "MB"]);
});
test(`引数 1024 ** 1 の戻り値は [1, "KB"] である`, () => {
const result = convertFileSize(1024 ** 1);
expect(result).toEqual([1, "KB"]);
});
test(`引数 1024 - 1 の戻り値は [1023, "byte"] である`, () => {
const result = convertFileSize(1024 - 1);
expect(result).toEqual([1023, "byte"]);
});
});
終わりに
テストコードを書いていれば、実装時の不具合チェックだけでなく、既存コードへの影響も検知できるので、コードの品質向上が期待できます!
テストで検証できていない実データやユーザー操作が存在する可能性があるので、
私は、テストコードを書いていれば完璧とは考えずに、あくまでもテストと思いながら、
テストコードを実装しています!
また、Testing Library は実際のブラウザ上には描画していないので、注意が必要だと考えています。
E2E テストでは、よりユーザー操作に近い形でテストができるので、テスト要件によっては E2E も選択肢に入ると思います。
Playwrightでもv1.22.0から コンポーネントテストもできるようになったので、こちらもチェックして記事にしてきたいと思います。
以上、参考になりましたら幸いです。