はじめに
react-testing-library を使うと React アプリケーションのコンポーネントの挙動をテストすることができます。
詳しくは公式ドキュメントを読んでください。
https://github.com/testing-library/dom-testing-library testing-library.com
ただ、react-testing-library は若干厄介なところがあり、 state の更新や非同期な処理が含まれる挙動をテストしたい場合に、 React と react-testing-library の挙動を理解していないとテストが思ったように動作しない、という経験をした人もいることと思います。
こういう場合に、 act
で処理を囲ったり、 waitFor
で処理を囲うことで「なぜか分からないけどテストが動いた」という経験がある人もいるのではないでしょうか。
僕もそうです。
react-testing-library の動きを少しでも理解を深めるために、よく対処方法として上がる act
と waitFor
について調べたのでまとめておきます。
react-testing-library
react-testing-library 自体には対したコード量はありません。
src ディレクトリ配下: react-testing-library/src at main · testing-library/react-testing-library · GitHub
react-testing-library 自体は、 react を render して HTML DOM を用意したり、イベント発火時に act を囲うようにする程度です。
実際のコンポーネントテストのイベントモック関数自体は testing-library の共通関数 (user-events
など) を使うことになると思います。
act の概要
React のコンポーネントをテストしようとした際に、該当の処理コードを act
で囲めよという警告が出ることがあります。
Warning: An update to XXX 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 */
React の act
については、警告の中にもある通り、このドキュメントに辿り着きます。
ただこれだけ読んでも分かるようで分かりません。 react-testing-library で act を呼び出しますが、実体は React 本体にあります。
- react-testing-library の act の実装はここになります。
- これは、 React (
react-dom
) のテスト用のユーティリティ(test-utils
) の act を呼んでいます。 react-dom
のtest-utils
の act はReact.unstable_act
を呼んでいます。- 結局
act
の本体は、 ReactAct というコードで、 React 本体が用意してくれているロジックになります。
元々出ていた警告も、 react-testing-library などの testing-library 系のコードが出力しているわけではなく、 React 本体のコードが出力しています。
act
については、この記事や記事内でも参照されている Example を読むと概要が理解できます。
簡単にまとめると以下だと理解しました。
- React の状態(ステート)の更新があった際に仮想 DOM への反映が同期的に行われても、実 DOM へのレンダリングコミットは非同期的に行われている。
- 非同期で行われる全ての実 DOM のレンダリングコミットの完了を待たずに
expect
してしまうと、意図しない実 DOM の UI 状態でテストをしてしまう。 act
メソッドで該当のレンダリング処理が走るコールバックを囲うことで、コールバック内での実 DOM 反映のレンダリングコミット処理が全て完了するのを待つ。- これにより、 テストするための実 DOM の UI 状態が意図したものになり、正しく
expect
を実施することができる。
act の内部実装
さらに実際の act
の内部の動きは、この記事に詳しく解説されています。
act
関数ではおおよそ以下の動きをしているようです。
ReactCurrentActQueue
というキューを用意しておく。- 与えられたコールバックを実行してレンダリング処理をした際に発生した状態更新による Fiber の UI 更新処理を
ReactCurrentActQueue
にキューイングする。 - (ここから正確に読み解けなかったけど) スケジューリングされた UI の更新時に、キューイングしておいたものを
flushActQueue
関数で全て実行する。 - ↑で flush し終えたら act を resolve する。
こういう動きが内部的に行われているので、 act
によって状態の整合性が正しく保たれることが(なんとなくですが)理解できました。
ちなみに、冒頭の warning が表示されている箇所では ReactCurrentActQueue.current
が null
である(つまりキューが初期化されていない)ことを確認しているようです。
Fiber の UI 更新処理が実施されるタイミングで、キューが空ならおかしいってことなんでしょうか(わからん)。 React-testing-library のテストの時はわかったんですが、普通に npm run dev の時も内部的に act されているってことですか?詳しい人教えてください。
user-event
は act を呼んでいる
react-testing-library のコンポーネントテストを実現する際には、testing-library
の user-event
を使ってユーザーの操作イベントをモックすることがあります。
軽く調査すると user-event
は act
で囲われているので自前で囲う必要はない、といった内容の記述が見られます。
これを具体的にコードで追うと、
user-event
は dispatchEvent
で呼び出される wrapEvent
で @testing-library/dom
の設定値である eventWrapper
を呼び出します。
その eventWrapper
という設定値を react-testing-library
で定義しています。
ここで指定されたコールバックを act で囲うようになっています。
これによって userEvent.click
を何も気にせずに使ったとしても、 act で囲われた状態で利用することができています。
ステートが非同期で更新されるケースには act では対応できない
userEvent.click
が act で囲われているにも関わらず、 act
で囲えという警告が出ることがあります。
これは大抵の場合、ステートが非同期に更新されてしまっていることによるものだと考えられます。
例えば、
上記のように、レンダリングコミットではなくステートの更新自体が非同期的に行われてしまうと、 act では対応しきれないようです。
これを対処するためには、 act で sleep 処理を挟むという手が取れそうです。
await act(async () => { await sleep(1100); // wait *just* a little longer than the timeout in the component });
react-act-examples/sync.md at master · threepointone/react-act-examples · GitHub
これによって act で挟んでいる sleep 中に API のレスポンスが返ってきて UI が更新されれば、 expect が正しく動くことにはなりそうです。
waitFor を使う
しかし act で sleep を囲うくらいなら、 waitFor を使うという手が取れそうです。
waitFor のコードは以下です。
dom-testing-library/src/wait-for.js at main · testing-library/dom-testing-library · GitHub
デフォルトの設定では、 1秒間の間 50 ms ごとに expect を繰り返すことで DOM 反映を待ってくれます。
これを挟み込んでおけば非同期的なステート更新にもおおよそ耐えうることになりそうですね。
ただ、本来的には時間に左右されるような処理にはさせたくないですが、ある程度仕方のないことかもしれません。
findBy 系のメソッドは waitFor も実行している
ちなみに findByRole
のような findBy
系のメソッドは waitFor を呼んでいます。
Async Methods | Testing Library
これによって非同期的な UI 更新がなされる場合、 getByRole
では動かなかったテストコードが findByRole
では動くということが起きます。
おわりに
react-testing-library の act
と waitFor
、またそれに関わるメソッドなどについて調査しました。
- act: React が提供する実 DOM へのレンダリングコミットを待ってくれる仕組み
- waitFor: 1秒間の間にたくさん expect しながら待ってくれる仕組み
また分からなくなったときに気が向いたら追調査してみようと思います。