はじめに
お疲れ様です、りつです。
今回はReact Testing Libraryでテストケースを実装中に「なるほどなぁ」と思ったtipsをご紹介します。
一言でいうと、DOM要素をwaitFor
してからgetBy
したい場合は、findBy
で書き直すこともできるというお話です。
このことを知った経緯について順を追って説明します。
ローディング完了後の画面タイトルを取得しようとするがエラー発生
今回、こちらの学習記録アプリのテストケースを実装しています。
テスト対象となるApp
コンポーネントの内容は以下の通りです。
コンポーネントのソースコード
import { useEffect, useState } from "react";
import "./App.css";
import { addRecord, deleteRecord, getAllRecords } from "../utils/supabaseFunctions";
function App() {
const [isLoading, setIsLoading] = useState(true);
const [title, setTitle] = useState("");
const [time, setTime] = useState(0);
const [records, setRecords] = useState([]);
const [error, setError] = useState("");
const [totalTime, setTotalTime] = useState(0);
const getRecords = async () => {
const records = await getAllRecords();
setRecords(records);
// 合計時間の計算
const totalTime = records.reduce((accumulator, currentValue) => accumulator + parseInt(currentValue.time), 0);
setTotalTime(totalTime);
setIsLoading(false);
}
// 画面初期表示
useEffect(() => {
getRecords();
}, []);
const onChangeTitle = event => setTitle(event.target.value);
const onChangeTime = event => setTime(event.target.value);
const onClickAdd = async () => {
if (title === "" || time === "" || time === 0) {
setError("入力されていない項目があります");
return;
}
const result = await addRecord({
title,
time
});
if (result.status !== 201) {
setError("登録処理に失敗しました");
return;
}
getRecords();
// 初期化
setTitle("");
setTime(0);
setError("");
}
const onClickDelete = async (id) => {
if (confirm("削除します。よろしいですか?")) {
const result = await deleteRecord(id);
if (result.status !== 204) {
setError("削除処理に失敗しました");
return;
}
getRecords();
// 初期化
setError("");
}
}
if (isLoading) {
return <>
<h1>Loading...</h1>
</>;
} else {
return <>
<h1 data-testid="title">学習記録一覧</h1>
<p>
学習内容
<input type="text" value={title} onChange={onChangeTitle} />
</p>
<p>
学習時間
<input type="number" min="0" value={time} onChange={onChangeTime} />
時間
</p>
<p>入力されている学習内容:{title}</p>
<p>入力されている時間:{time}時間</p>
<ul>
{records.map((record) => (
<li key={record.id}>
{`${record.title} ${record.time}時間`}<button onClick={() => onClickDelete(record.id)}>削除ボタン</button>
</li>
) )}
</ul>
<button onClick={onClickAdd}>登録ボタン</button>
{error !== "" && <p className="error">{error}</p>}
<p>合計時間:{totalTime} / 1000 (h)</p>
</>;
}
}
export default App;
今回実装したかったテストケースが、『タイトルが「学習記録一覧」であること』を確認するためのものだったのですが、以下のエラーに遭遇しました。
-
対象のコード
src/tests/componentApp.spec.jsximport App from "../App"; import React from "react"; import '@testing-library/jest-dom' import { render, screen } from "@testing-library/react"; describe("Title Test", () => { it("タイトルが「学習記録一覧」であること", async () => { render(<App />); const title = screen.getByTestId("title"); expect(title).toHaveTextContent("学習記録一覧"); }); });
エラーの原因
この学習記録アプリは、supabaseからデータを取得して画面の初期表示を行うのですが、データ取得完了まではローディング画面が表示されます。
今回のテストケースでは、以下のDOMのテキストを取得したかったのですが、
<h1 data-testid="title">学習記録一覧</h1>
以下のテストケースが実行された時点では、まだローディング画面の状態のままであり、
const title = screen.getByTestId("title");
DOMの状態としては、エラー内容に記載の通り以下の状態となっていました。
<body>
<div>
<h1>
Loading...
</h1>
</div>
</body>
この状態だと、先ほどのscreen.getByTestId("title");
で取得しようとしているdata-testid="title"
が見つからないためエラーとなっています。
対処方法 ①
まず最初に試したのが以下のように、waitFor
を使用する方法です。
これにより、DOMにdata-testid="title"
が表示されるまで待ったうえで、タイトルを取得することができます。
import App from "../App";
import React from "react";
import '@testing-library/jest-dom'
import { render, screen, waitFor } from "@testing-library/react";
describe("Title Test", () => {
it("タイトルが「学習記録一覧」であること", async () => {
render(<App />);
// loading画面の表示が終わるまで待機
await waitFor(() => {
expect(screen.getByTestId("title")).toBeInTheDocument();
});
const title = screen.getByTestId("title");
expect(title).toHaveTextContent("学習記録一覧");
});
});
対処方法 ②
上記の方法でも特に問題はなさそうだったのですが、以下の記事を見つけました。
findBy
の説明部分を読んでみて、『先ほどのコードもfindBy
でいけるのでは?』と思いました。
まだ存在しないものの最終的に存在する要素については、getByやqueryByではなくfindByを使用してください。
結果的に、以下のように書き直すことでもテストがパスすることを確認できました。
import App from "../App";
import React from "react";
import '@testing-library/jest-dom'
import { render, screen } from "@testing-library/react";
describe("Title Test", () => {
it("タイトルが「学習記録一覧」であること", async () => {
render(<App />);
const title = await screen.findByTestId("title");
expect(title).toHaveTextContent("学習記録一覧");
});
});
ドキュメントを確認したところ、どうやらfindBy
は、getBy
とwaitFor
の合わせ技のようです。
おわりに
fidBy
をうまく使うとwaitFor
よりもすっきりしたコードを書くことができます。
今回初めてReactのテスト実装を行っているため、まだまだつかみ切れていない部分も多いですが引き続き学んでいきたいと思います。
参考