1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jest act()の勘違いのせいで2時間詰まった話

Posted at

はじめに

Jestを使用して、ページの初期表示、レコードの追加、削除のテストケースを書いています。

書いたコードと、発生したエラー

ひとまず非同期処理や画面更新を挟む処理の場合は、act()で囲っておくといいという認識で、以下のように記述してみました。

describe("Apptest", () => {
  it("Title test", async () => {
    await act(() => {
      render(<App />);
    });

    const titleElement = await screen.findByTestId("title");
    expect(titleElement).toHaveTextContent("今までの学習記録");
  });
});

describe("supabase test", () => {
  it("insertRecord が正常に呼び出され、記録が追加されていることをテストする", async () => {
    await act(async () => {
      render(<App />);
    });
.....

一通りテストケースを書いて、テストを通して見たところ、テストケースは全件パスできました。

…しかし、以下のようなエラーがコンソール画面に表示されていました。

  console.error
    An update to App inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):
    
    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
    
    This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

      13 |       const todos = await supabase.getRecords();
      14 |       if (todos.error) return setError("データの取得に失敗しました");
    > 15 |       setRecords(todos.data);
         |       ^
      16 |       setIsLoading(false);
      17 |     };
      18 |     getTodos();

      at node_modules/react-dom/cjs/react-dom-client.development.js:18758:19
      at runWithFiberInDEV (node_modules/react-dom/cjs/react-dom-client.development.js:874:13)
      at warnIfUpdatesNotWrappedWithActDEV (node_modules/react-dom/cjs/react-dom-client.development.js:18757:9)
      at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom-client.development.js:16409:11)
      at dispatchSetStateInternal (node_modules/react-dom/cjs/react-dom-client.development.js:9170:13)
      at dispatchSetState (node_modules/react-dom/cjs/react-dom-client.development.js:9127:7)
      at setRecords (src/App.jsx:15:7)

ざっくり言うと、useEffect内のsetState処理がact()で囲まれていませんよ、とのこと。

「あれ、ちゃんとレンダリング部分はact()で囲ったんだけどな…何かおかしなところあったのかな?」
「まさか、useEffectも全部Mock化しないといけない?めんどくさいなー…」
「そもそも、useEffectの更新もまとめて見てくれるのがact()じゃないの?」

と思いつつ色々検索してみたのですが、これといった解決策は見当たらず…

解決?

そうこうしてしばらく経ったころ、以下の記事を発見。

そこには、ざっくり言うとこう書いてありました。

renderをactで囲む必要はないよ。内部でact処理があるから

なるほど?ということで、上記コードのrender()を囲むact()を取り除いてみました。

describe("Apptest", () => {
  it("Title test", async () => {
    render(<App />);

    const titleElement = await screen.findByTestId("title");
    expect(titleElement).toHaveTextContent("今までの学習記録");
  });
});

describe("supabase test", () => {
  it("insertRecord が正常に呼び出され、記録が追加されていることをテストする", async () => {
      render(<App />);
.....

この状態で再びテストしてみたところ…
今度はエラーが出ずにテストをパスできました!

再び混乱へ

「なんか釈然としないけどまあいいや、ひとまずQiita記事に投稿しよう。とりあえずコードを戻して、エラーをもう一度出さないとな」

ということで、コピペでrenderをactで囲み直して再実行してみたところ…

さっきまで起こっていたAn update to App inside a test was not wrapped in act(...)のエラーが、なぜか起きなくなっていました。

「あれ!?なんでだ!?actで囲っていたからエラーが起きてたんじゃないのか!?」
と大混乱で、とにかくコードを修正しつつ、原因を模索することに…

真の原因

…というわけで、結論として、エラーが起きていたのは、コードの以下の部分が原因でした。

describe("Apptest", () => {
  it("Title test", async () => {
    await act(() => { 
    // ↑この行が原因
      render(<App />);
    });

    const titleElement = await screen.findByTestId("title");
    expect(titleElement).toHaveTextContent("今までの学習記録");
  });
});

describe("supabase test", () => {
  it("insertRecord が正常に呼び出され、記録が追加されていることをテストする", async () => {
    await act(async () => {
      render(<App />);
    });
.....

そもそもactは非同期(await)で呼ばれることを前提としているため、コールバック関数もasyncで非同期関数として定義しなければならず、asyncが抜けていたために、actがうまく働いていなかったのが原因、のようです。

ClaudeCodeに聞いてみたところ、詳細は以下の通りでした。

renderによって初期表示のレンダリングが行われる。render内にはactが内蔵されているので、エラーは起こらない。

renderによるレンダリングが終わったあと、useEffectが走る。この処理はrender()処理の後に行われるため、act内のコールバック関数をasyncで非同期化していないと、エラーになる。

ではなぜ、render()単体だとエラーにならないのか?それは、後続のfindBy...関数が、画面の更新を待機する処理であったために、useEffectによる画面更新も監視されていたから、とのこと。

で、renderをactで囲み直したときにエラーが再現できなかったのは、コード下部の、ちゃんとasyncがついている方のactをコピペしたからのようです。

ざっくりまとめると、以下のようになります。

render(<App />);→エラーが起きない
await act(() => { render(<App /> });エラーが起きる
await act( async () => {render(<App />)});→エラーが起きない

まとめ

asyncを入れ忘れただけで、これほど手こずるとは思いませんでした。
そして、そもそもrenderをactで囲む必要すらもなかったということで…
改めて、「なんとなく」でコードを書くことの危うさを思い知りました。
また、自分にコードをざっくり斜め読みしてしまう癖があるために、このミスに気づくのにも時間がかかりました。
素早く読めることは、コーディングにおいて大切なスキルではありますが、不具合発見の場においては、スピードよりも「見落とさないこと」のほうが大事だということをひしひしと感じました。

JISOUのメンバー募集中!

プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてください!
▼▼▼

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?