フロントエンドテスト 入門 React Testing Library編
今回は、フロントエンドテストの方法について解説していきます。
フロントエンドでは様々なテストがありますが、今回はReact Testing Libraryの使い方に絞って解説していきます。
以前にJest編の記事も作ったので、そちらの続きとして進めていこうと思います。
Hello World
まずは、Reactのテストがどのように実行されるかを簡単に解説していきます。
create-react-app
でプロジェクトを作ったと仮定して進めていきます。
新しくコンポーネント用のファイルを作成します。
const Hello = () => {
return <h1>Hello World</h1>;
};
export default Hello;
次のこのコンポーネントが、正しく表示されているかをテストします。
import { render, screen } from "@testing-library/react";
import Hello from "./Hello";
test("h1が存在するか", () => {
render(<Hello />);
const h1EL = screen.getByText("Hello World");
expect(h1EL).toBeInTheDocument();
});
まず、render
メソッドでコンポーネントを擬似的にレンダリングすることができます。
そして、レンダリングされた画面をscreen
から取得することができます。
最後に、取得した要素が想定通りにあるかをテストしています。
ちなみにTypescriptを使う場合は、拡張子をtsではなくtsxにしないとエラーになるみたいなので注意です。
要素の取得
先ほどはgetByTextを使って要素を取得しましたが、他にも取得するための関数はいくつかあります。
まず大前提として、次の3種類に分けることができます。
- getBy
- queryBy
- findBy
getBy
は要素を探しにいき、見つからなければエラーが返されます。
次に、queryBy
ではnullが返ってきます。
そして、最後のfindBy
は非同期関数に使用することができます。
これについては後ほど解説します。
ちなみに、~ByAll
とすることで、対象の要素を全て取得することができます。
そして、要素取得用の主な関数をまとめると、次のようになります。
- getByRole
- getByLableText
- getByPlaceholderText
- getByText
- getByDisplayValue
- getByAltText
- getByTitle
- getByTestId
一応、上から順に優先的に使うべきとされています。
詳しい説明はこちらの記事を参考にしてください。
マッチャー
次に、DOMに関するマッチャーについて解説していきます。
よく使うものを表にまとめると、次のようになります。
マッチャー | 説明 |
---|---|
toBeDisabled | disabledかチェック |
toBeEnabled | disabledじゃないかチェック |
toBeEmptyDOMElement | DOMの中身が空かチェック |
toBeInTheDocument | DOMの中身に対象があるかチェック |
toBeInvalid | input要素などがinvalidかチェック |
toBeRequired | input要素などrequiresかチェック |
toBeValid | input要素などがinvalidではないかチェック |
toBeVisible | 要素が目に見える状態かチェック |
toContainElement | 要素を子要素として持っているかチェック |
toContainHTML | 要素を子要素として持っているかチェック |
toHaveClass | 対象のクラスを持っているかチェック |
toHaveFocus | focus状態かチェック |
toHaveFormValues | formの値をチェック |
toHaveStyle | 対象のstyleをチェック |
toHaveTextContent | 中の文字要素をチェック |
toHaveValue | inputなどのvalueをチェック |
toBeChecked | チェック状態かチェック |
User Event
次は、ユーザーのインタラクションな操作に対するテストの方法を解説していきます。
まず、react-testing-libraryにもfireEvent
というイベントの発火を再現できる関数がありますが、これはあまり使わない方が良いです。
なぜなら、ユーザーの操作がさらに忠実に再現されているuserEvent
という関数があるからです。
なので、ユーザーの操作をテストしたい時は、このuserEvent
を使用するようにしましょう。
では早速、ユーザーが入力した値を画面に表示するコンポーネントを作成します。
import React from "react";
const Input = () => {
const [value, setValue] = React.useState<string>("");
return (
<div>
<h1>入力された文字: {value}</h1>
<input type="text" onChange={(e) => setValue(e.target.value)} />
</div>
);
};
export default Input;
次に、この入力された値が問題なく画面に表示できているかをテストしていきます。
import { render, screen } from "@testing-library/react";
import Input from "./Input";
import userEvent from "@testing-library/user-event";
test("input", () => {
render(<Input />);
// 要素の取得
const h1El = screen.getByRole("heading");
const inputEl: HTMLInputElement = screen.getByRole("textbox");
// 初期状態のテスト
expect(h1El.textContent).toBe("入力された文字: ");
expect(inputEl.value).toBe("");
// 値の入力
userEvent.type(inputEl, "test");
// 入力後のテスト
expect(h1El.textContent).toBe("入力された文字: test");
expect(inputEl.value).toBe("test");
});
このように、input要素やボタン要素などのインタラクション系のテストも、testing-libraryを使用して実行することができます。
非同期処理
次に、非同期処理のテストについて見ていきます。
まずは、非同期処理を使用したコンポーネントを作成します。
import React, { useEffect } from "react";
import "./App.css";
function App() {
const [value, setValue] = React.useState<any>({});
useEffect(() => {
fetch("https://qiita.com/api/v2/items")
.then((res) => {
return res.json();
})
.then((data) => {
setValue(data[0]);
});
}, []);
return <p>{value.title ? `Title is ${value.title}` : null}</p>;
}
export default App;
そして、このコードのテストファイルを作成します。
import { render, screen } from "@testing-library/react";
import App from "./App";
test("render App", () => {
render(<App />);
const linkElement = screen.queryByText(/Title is/i);
expect(linkElement).toBeNull();
});
このように、普通にrender
しても非同期関数がある場合は、fetch前の画面をレンダリングすることになります。
なので、Title is
という文字列は見つからずに、nullが返ってきます。
では、データ取得後のコンポーネントをテストしたい場合はどうしたら良いかと言うと、findBy
メソッドを使用すればOKです。
先ほども少し説明しましたが、findBy
メソッドはデータ取得まで待機させることができます。
なので、次のようにテストコードを書けばOKです。
import { render, screen } from "@testing-library/react";
import App from "./App";
test("render App", async () => {
render(<App />);
const linkElement = await screen.findByText(/Title is/i);
expect(linkElement).toBeInTheDocument();
});
find系のメソッドは非同期関数なので、async/awaitを忘れないようにしましょう。
カスタムフック
React testing-libraryには、カスタムフック専用のテストも存在します。
それは、act
とrenderHook
です。
その関数を試すために、まず簡単なカスタムフックを定義します。
import { useState } from "react";
export const useCounter = () => {
const [count, setCount] = useState(0);
const increment = () => setCount((prev) => prev + 1);
const decrement = () => setCount((prev) => prev - 1);
return { count, increment, decrement };
};
今回は、簡単なカウンターのhooksを用意しました。
そして、これのテストコードは次のようになります。
import { renderHook } from "@testing-library/react";
import { useCounter } from "./useCounter";
import { act } from "react-dom/test-utils";
describe("useCounter", () => {
it("should be increment", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it("should be decrement", () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
});
このように、まずrenderHook
でhooksの初期化ができます。
次にact
を使用することで、hooksに定義された関数を実行することができます。
なので、先ほどのようにテストを書くことで、hooksのテストもすることができるのです。
まとめ
今回は、Reactのtesting-libraryを使用したテストの方法を解説してきました。
今やほとんどの現場で使用されている技術なので、この機会に身につけましょう。
宣伝
0からエンジニアになるためのノウハウをブログで発信しています。
また、YouTubeでの動画解説も始めました。
インスタの発信も細々とやっています。
興味がある方は、ぜひリンクをクリックして確認してみてください!
Discussion