VeeValidateを利用したコンポーネントをJestでテストする


はじめに

Vue v3及び、CompositionAPIで作られたコンポーネントでvee-validate v3系を対象に、Jestでテストを書きます

以下のようなコンポーネントを想定

  • validationProviderでinputをvalidate
  • validationObserverで各Providerを監視
child-input.vue
<template>
  <validation-provider mode="eager" rules="required">
    <input
      v-model="val"
      name="input"
    >
    <ul v-if="errors.length" class="errors">
      <li v-for="(error, i) in errors" :key="i">
        {{ error }}
      </li>
    </ul>
  </validation-provider>
</template>

<script lang="ts">
export default {
  setup(_, { emit }) {
    const val = ref<string>('')
    watch(() => {
      emit('update', val.value)
    })
    return { val }
  }
}
</script>
parent-form.vue
<template>
  <validation-observer ref="observer">
    <form>
      <child-input @update="update" />
    </form>
  </validation-observer>
</template>

<script lang="ts">
export default {
  setup(_, { emit }) {
    const val = ref<string>('')
    const update = (value: string) => {
      val.value = value
    }
    return {
      val,
      update
    }
  }
}
</script>

テスト

それではテストを書いていきます

form-validation.spec.ts
import { createLocalVue, mount } from '@vue/test-utils'
import { ValidationObserver, ValidationProvider, extend } from 'vee-validate'
import ParentForm from '@your/path/parent-form.vue'

const localVue = createLocalVue()
localVue.component('ValidationObserver', ValidationObserver)
localVue.component('ValidationProvider', ValidationProvider)

const { required } = require('vee-validate/dist/rules.umd')
extend('required', required)

jest.useFakeTimers()

describe('フォームのバリデーションチェック', () => {
  test('未入力でバリデーションされること', async (): Promise<any> => {
    const wrapper = mount(ParentForm, { localVue })
    const observer: InstanceType<typeof ValidationObserver> = wrapper.vm.$refs.observer
    await observer.validate()
    jest.runAllTimers()
    await wrapper.vm.$nextTick()
    expect(wrapper.find('.errors').exists()).toBeTruthy()
  })
  test('入力が完了すること', async (): Promise<any> => {
    const wrapper = mount(ParentForm, { localVue })
    wrapper.find('input').setValue('hoge')
    const observer: InstanceType<typeof ValidationObserver> = wrapper.vm.$refs.observer
    await observer.validate()
    jest.runAllTimers()
    await wrapper.vm.$nextTick()
    expect(wrapper.find('.errors').exists()).toBeFalsy()
  })
})

テストの解説

それぞれの処理について見ていきます。

const wrapper = mount(ParentForm, { localVue })

今回は子コンポーネントもテスト対象のためshalowMountではなくmountを利用しています。古い記事を参照するとmountのオプションに{ sync: false }を追加するという情報がありますがそのオプションは廃止されています。
https://github.com/vuejs/vue-test-utils/issues/1137

const observer: InstanceType<typeof ValidationObserver> = wrapper.vm.$refs.observer
await observer.validate()

ValidationObserverインスタンスを取得してvalidateメソッドを実行します。これによってvee-validateのバリデーションが実行されます。

jest.runAllTimers()

ValidationObserverの処理に16msかかるそうで、それを待ちます。
https://logaretm.github.io/vee-validate/advanced/testing.html#testing-validationobserver-debounced-state

await wrapper.vm.$nextTick()

先程{ sync: false }オプションがなくなったことで画面描画を待つために、$nextTickを利用します。このメソッドがPromiseを返すためawaitで待つことによってこの後の処理ではDOMに反映された状態を取得することができます。
vee-validate公式ではflush-promisesを使う例を紹介されていますが$nextTickを利用することで同様の結果を得られるので不要になるかと思います。

まとめ

  • syncオプションはもう使えない
  • flush-promisesではなく$nextTickを使おう