はじめに
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では、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてください!
▼▼▼