※本記事は VeeValidate v2向けです。v3でのユニットテストのやり方については 公式ドキュメント を参照してください。
VeeValidateを使ったコンポーネントの書き方には、注意が必要なポイントがあります。特に、vue-test-utilsと組み合わせて使っている場合、謎のエラーに悩まされる可能性があります。
先に結論
-
mount
/shallowMount
はsync: false
にすること -
flush-promises
等で、バリデーションの完了を待つこと
VeeValidate + vue-test-utilsのありがちなエラー
以下のようなコンポーネントがあります。nameフィールドが未入力(null/undefined/空文字列)だと、エラーメッセージが表示されます。
<template>
<form>
<label>
Name: <input name="name" v-model="name" v-validate="'required'" />
</label>
<p style="color:red">{{ errors.first('name') }}</p>
</form>
</template>
<script>
export default {
name: 'EntryForm',
data() {
return {
name: '',
}
}
}
</script>
このフォームのバリデーションをテストしてみます。mocha + chai + vue-test-utilsでテストを書くと、こんな感じになると思います。テスト用のVueコンストラクタをcreateLocalVue
で作成し、これにVeeValidateをインストールしているところがミソです。
import { expect } from 'chai'
import { shallowMount, createLocalVue } from '@vue/test-utils'
import VeeValidate from 'vee-validate'
import EntryForm from '@/components/EntryForm.vue'
const localVue = createLocalVue()
localVue.use(VeeValidate)
describe('EntryForm', () => {
it('The name field is required', async () => {
const wrapper = shallowMount(EntryForm, {
localVue,
})
wrapper.find('[name="name"]').setValue('')
wrapper.vm.$validator.validateAll()
expect(wrapper.vm.errors.first('name')).to.equal('The name field is required.')
})
})
このテストを実行してみると、Macだとエラーが大量に表示され、WindowsだとStack Overflow(exit code 3221225725)が発生します。
エラーの原因は、vue-test-utilsがデフォルトでDOMの更新を同期的に適用するのに対して、VeeValidateは非同期なDOMの更新(Vueのデフォルトの動作)を期待しているからです。この辺の事情はvue-test-utilsドキュメントの↓の記事に詳しいです。
https://vue-test-utils.vuejs.org/ja/guides/testing-async-components.html
テストが動くようにする
上記記事にも言及があるように、vue-test-utilsでDOMの更新を非同期的に適用するようにするには、shallowMount
の第2引数に { sync:false }
を渡します。
const wrapper = shallowMount(EntryForm, {
localVue,
+ sync: false,
})
この状態で再度実行すると、先ほどのエラーは消えますが、テストは依然として失敗します。バリデーションエラーになるはずなのに、エラーにならない、という状態になります。これは、バリデーションの反映が非同期に行われるためです。そこで、flush-promises
ライブラリ等を使用して、全ての非同期処理が完了するのを待ちます。
import { expect } from 'chai'
import { shallowMount, createLocalVue } from '@vue/test-utils'
import VeeValidate from 'vee-validate'
+import flushPromises from 'flush-promises'
import EntryForm from '@/components/EntryForm.vue'
const localVue = createLocalVue()
@@ -15,7 +14,6 @@ describe('EntryForm', () => {
})
wrapper.find('[name="name"]').setValue('')
wrapper.vm.$validator.validateAll()
+ await flushPromises()
expect(wrapper.vm.errors.first('name')).to.equal('The name field is required.')
})
})
これで、テストが正常に実行されるようになります。
EntryForm
✓ The name field is required (42ms)
1 passing (45ms)
MOCHA Tests completed successfully
まとめ
冒頭の結論を再掲します:
-
mount
/shallowMount
はsync: false
にすること -
flush-promises
等で、バリデーションの完了を待つこと
flush-promisesはvue-test-utilsで非同期処理のテストを書いていれば頻出のライブラリですが、VeeValidateがsync:false
でないと動かないのは落とし穴ですね。