Movatterモバイル変換


[0]ホーム

URL:


Mitsuyuki.Shiiba

React のテストを書いてたら act で囲んでよーって言われたとき

React のコンポーネントのテストを書いてたら、テストは成功してるんだけど、こういう感じの Warning が出力されるって場合がある

Warning: An update to Counter 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 */

ステートを変更するときはact で囲んでねって書いてある。だから、囲めばいいのかなぁ?って思ってたら、ちょっと触った感じ、どうやらそういうことでもないみたい。ので、うろうろしてたら、この2つの記事にたどり着いた

React のactAPI の説明

React Testing Library の作者である Kent C. Dodds の記事

なるほどねぇ

再現コード

例えばこんなコードを書けば再現することができる。この Kent のYouTube 動画を見て書いた →KCD Office Hours 2020-11-19 - YouTube

import{ useState}from"react";import{ render, screen}from"@testing-library/react";import userEventfrom"@testing-library/user-event";function Counter(){const[count, setCount]= useState(0);const increment=()=> setTimeout(()=> setCount((c)=> c +1),0);return(<buttontype="submit"onClick={increment}>{count}</button>);}test("my thing works",async()=>{  render(<Counter />);  userEvent.click(screen.getByText("0"));awaitnewPromise((resolve)=>{    setTimeout(resolve,50);});});

最後の await は、Warning が出る前にテストが終了しちゃわないように待ってるだけ。

React のactAPI の説明

じゃ、1つ目の記事の方から。React のactAPI の説明。

フックを使った処理は同期でレンダリングされるわけじゃなくて、Fiber では非同期でいい感じに処理される。だから、例えばレンダリング時にステートを変更して再描画される場合は、render の直後にアサーションを書いても、そのステートの変更がまだ反映されてなかったりする。

↓React 16 で React Fiber っていうアーキテクチャで中身を書き直したらしい。へー。あとでゆっくり読もっと。

ReactはなぜFiberで書き直されたのか?Reactの課題と将来像を探る | HTML5Experts.jp

「だからテスト用にact を用意したよ!act の中でイベントを発生させたら、その中でステートの更新とかを終わらせてしまうよ。なので直後にアサーション書いても大丈夫だよ!」ということか。なるほど。

他にも、テストではOKなのに実際に動かしたら最適化のせいでうまく動かないコード、とかもact で囲むことで、テストの時点で検知できるようになるっぽい。へー。

非同期処理は?

ところで、↑で書いたカウンターの例みたいに、ステートの更新処理自体が非同期処理の場合はどうなるんだろう?というのが、今日のメインの話。

act の中でユーザーイベントとかを実行しても、その処理の中で Promise を使ってたりすると、act を抜けたあとでステートの更新が走ってしまう。そうすると冒頭に書いた Warning が出る。分かりにくいなw

ということで、そういう処理をact の中で呼び出したあとに、例えばこんな風にact の中で待てば、その間にステートの更新が行われて、想定通りのテストを実行することができる。ということが↑の記事に書いてある。

  await act(async () =>{    await sleep(1100);// wait *just* a little longer than the timeout in the component});

なるほどー。

React Testing Library

じゃ、次。Kent の記事を読もう。

React Testing Library (React Testing Library | Testing Library) は、内部でact でラッピングしてるから、RTL の関数を使う場合はact は書かないでいいよ。ってことみたい。

この辺かな?fireEvent とかrender もラッピングしてある:

https://github.com/testing-library/react-testing-library/blob/0db811283819fdc9774e36155ff806f44500533c/src/pure.js#L11-L26

configureDTL({  asyncWrapper: async cb =>{let result    await asyncAct(async () =>{      result = await cb()})return result},  eventWrapper: cb =>{let result    act(() =>{      result = cb()})return result},})

https://github.com/testing-library/react-testing-library/blob/0db811283819fdc9774e36155ff806f44500533c/src/pure.js#L59-L65

  act(() =>{if (hydrate){      ReactDOM.hydrate(wrapUiIfNeeded(ui), container)}else{      ReactDOM.render(wrapUiIfNeeded(ui), container)}})

あと、便利ツール的な user-event もact でラッピングされてるみたいね。

はて?ということは?

test("my thing works", async () =>{  render(<Counter />);  userEvent.click(screen.getByText("0"));  awaitnew Promise((resolve) =>{    setTimeout(resolve, 50);});});

このコードのuserEvent.clickact でラッピングされているんだけど、その中で呼び出されてるカウンターのインクリメント処理が非同期だから、act を抜けたあとにカウンターの更新処理が走って、冒頭に書いた Warning が出てるということか。

つまり、この最後の Promise をact で囲めば Warning は消えそうだな。

test("my thing works", async () =>{  render(<Counter />);  userEvent.click(screen.getByText("0"));  await act(async () =>{    awaitnew Promise((resolve) =>{      setTimeout(resolve, 50);});});});

うん。消えた。

けど嫌だよね?

なんか時間に依存するのは、Flaky Test になるので嫌だー。よね?ということで、RTL のwaitFor という関数を使えば OK。

waitFor 自体は dom-testing-library というライブラリで実装されてる

のだけど、RTL がこれをact でラッピングしてるのだ。さっき書いたコードのconfigureDTL の部分だと思う。

だから、RTL がラッピングしたバージョンのwaitFor を使えば、act の中で待ってるということになる。ということで、↓のように書けば「カウンターがインクリメントされるまでact の中で待つ」という意味になるから、Warning も出ないし、Flaky Test にもならないし、平和が訪れた。

test('my thing works', async () =>{  render(<Counter />);  userEvent.click(screen.getByText('0'));  await waitFor(() => expect(screen.getByText('1')).toBeInTheDocument());});

ちなみに、このwaitForgetByText 的なのを組み合わせた関数としてfindBy を提供してくれてるので、こういう風に書く方がきれいかな

test('my thing works', async () =>{  render(<Counter />);  userEvent.click(screen.getByText('0'));  expect(await screen.findByText('1')).toBeInTheDocument();});

すっきりしたー!waitFor できる何かが存在してないといけないけどね。

フリーレン買おっと。思い出すために前の巻を読んでる途中で寝てしまうんだろうな。おやすみなさい。

プロフィール
プロフィール画像
株式会社カケハシでソフトウェアエンジニアをやってます。
検索

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です読者をやめる読者になる読者になる

[8]ページ先頭

©2009-2025 Movatter.jp