これはなに
フロントエンドのテストを書く上で、非同期処理の扱いはハマりポイントの1つに挙げられると思います。自分もよく非同期処理が詰まっていて期待した挙動にならないということがあったので、非同期処理の扱いについて備忘録的にまとめてみようと思います。
もっと良い方法などあれば、コメントいただけると嬉しいです🙏
環境
- Vue.js 3.1.4
- Jest 24.9.0
- @vue/test-utils 2.0.0-rc.1
なぜ非同期処理を意識する必要があるか
例えば以下のようなケースを考えてみましょう。
// 画面上にHello Worldを表示する
expect(wrapper.text()).toMatch('Hello World')
Hello World が表示されていることをテストしていますが、仮に // 画面上にHello Worldを表示する
の部分が非同期処理だった場合、 Hello World が表示される前に expect(wrapper.text()).toMatch('Hello World')
の検証をしてしまうかもしれません。そうなった場合、 Hello World はまだ表示されていないため、テストは失敗してしまいます。
どのような時に非同期処理になるか
では、どのような時に非同期処理になるのでしょうか。APIが叩かれた時に処理が非同期になるのは、比較的イメージしやすいんじゃないかなと思います。他にも処理が非同期になるケースがあるので、ここでは3つピックアップしてみます。
trigger
クリックイベントなど何らかのイベントを発火させたい時に使用する。
wrapper.find('button').trigger('click')
setProps
mount もしくは shallowMount した後に props を更新したい時に使用する。
wrapper.setProps({ hoge: 'hoge' })
setValue
v-model と紐づいている input 要素の値を入力したい時に使用する。
wrapper.find('[data-test="hoge"').setValue('test')
非同期処理を流す方法について検討する
今回挙げた3パターンはどれも Promise を返すので、async/await を使うようにすれば、反映された後の状態でテストすることができます。ただし、複数の非同期処理が積まれている場合(登録ボタン押下後、登録用のAPIが叩かれるなど)は、後述する方法で明示的に非同期処理を流してあげる必要があります。
nextTick
nextTick は 非同期処理を1つだけ 流すことができます。
そのため、仮に非同期処理が2つ積まれている場合は、nextTick が2つ必要になります。
import { nextTick } from 'vue';
test('○○になること', async () => {
// なんらかの非同期処理
// 非同期処理を1つ流す
await nextTick()
...
});
flushPromises
flushPromises は 非同期処理を全て 流すことができます。
そのため、複数の非同期処理が積まれている場合でも、flushPromises は1つのみでOKです。
ただし、flush-promisesを別途インストールする必要があります。
import flushPromises from 'flush-promises';
test('○○になること', async () => {
// なんらかの非同期処理
// 非同期処理を全て流す
await flushPromises()
...
});
nextTick と flushPromises のどちらを使えば良い?
自分は基本的に nextTick を使うようにしています。ただし、 初期化時の処理が全部終わってから後続処理をやりたい場合 は、mount 直後に flushPromises を使用することがあります。
flushPromises をあまり使わない理由は、非同期処理が意図せず増えてしまった場合に気付けない可能性があるからです。とはいえ、実際に flushPromises を使って困った経験はないので、この辺りはより良い方法を模索中です。
ちなみに改めて公式ドキュメントを見てみたところ、このような記述がありました。
- Use await nextTick() to ensure the DOM has updated before the test continues
- Functions that might update the DOM (like trigger and setValue) return nextTick, so you need await them.
- Use flush-promises from Vue Test Utils to resolve any unresolved promises from non-Vue dependencies (such as API requests).
これを見る限り、DOMを更新する場合は、 nextTick で、Vue以外の依存関係(APIを叩くなど)の場合は flushPromises を推奨してそうな雰囲気がありますね。他の方々の使用方法も気になるので、こんな使い分けをしているなどあれば、コメントしていただけると嬉しいです!
まとめ
ここまで非同期処理になるケースと、対処方法についてまとめました。テストを書いていて意図した挙動にならない時に「とりあえず nextTick を入れてみたら動いた」みたいなパターンはよくあるので、今後さらに非同期処理について理解を深めていきたいと思っています。
おまけ
今回の記事を書くにあたり、実際に試しながら処理が非同期になる処理を調査していました。
個人的に非同期処理だと思い込んでいたが、実は非同期処理ではなかったというものをメモとして残しておきます。
子コンポーネントから emit する
子コンポーネントからイベントを emit させたい時に使用する。
ぱっと見、非同期処理だと思っていたが違った。
wrapper.findComponent(ChildComponent).vm.$emit('hoge')