2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Testing LibraryでLoading画面終了後の画面表示テキストを取得する方法

Last updated at Posted at 2024-11-12

はじめに

お疲れ様です、りつです。

今回はReact Testing Libraryでテストケースを実装中に「なるほどなぁ」と思ったtipsをご紹介します。

一言でいうと、DOM要素をwaitForしてからgetByしたい場合は、findByで書き直すこともできるというお話です。

このことを知った経緯について順を追って説明します。

ローディング完了後の画面タイトルを取得しようとするがエラー発生

今回、こちらの学習記録アプリのテストケースを実装しています。

テスト対象となるAppコンポーネントの内容は以下の通りです。

コンポーネントのソースコード
src/App.jsx
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.jsx
    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 = screen.getByTestId("title");
        expect(title).toHaveTextContent("学習記録一覧");
      });
    });
    
  • エラー内容
    image.png

エラーの原因

この学習記録アプリは、supabaseからデータを取得して画面の初期表示を行うのですが、データ取得完了まではローディング画面が表示されます。

今回のテストケースでは、以下のDOMのテキストを取得したかったのですが、

src/App.jsx
<h1 data-testid="title">学習記録一覧</h1>

以下のテストケースが実行された時点では、まだローディング画面の状態のままであり、

src/tests/componentApp.spec.jsx
const title = screen.getByTestId("title");

DOMの状態としては、エラー内容に記載の通り以下の状態となっていました。

<body>
  <div>
    <h1>
      Loading...
    </h1>
  </div>
</body>

この状態だと、先ほどのscreen.getByTestId("title");で取得しようとしているdata-testid="title"が見つからないためエラーとなっています。

対処方法 ①

まず最初に試したのが以下のように、waitForを使用する方法です。
これにより、DOMにdata-testid="title"が表示されるまで待ったうえで、タイトルを取得することができます。

src/tests/componentApp.spec.jsx
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を使用してください。

結果的に、以下のように書き直すことでもテストがパスすることを確認できました。

src/tests/componentApp.spec.jsx
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は、getBywaitForの合わせ技のようです。

おわりに

fidByをうまく使うとwaitForよりもすっきりしたコードを書くことができます。

今回初めてReactのテスト実装を行っているため、まだまだつかみ切れていない部分も多いですが引き続き学んでいきたいと思います。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?