Vue3 コンポーネントのテスト
コンポーネントのテストをするのフレームワークとしてJest、vue-test-utils 2を利用します。Vue2系をターゲットにしているvue-test-utils 1とは一部APIが異なります。
Component の単体テストは書くべきでしょうか?UI のテストは割りに合わない、ロジックはコンポーネントの外のモジュールに記述している(特に composition API では)などの理由から、コンポーネントに対するテストは書かない選択肢もあるでしょう。
とはいえ、やはりテストが書いてあると変更を加えた際に安心できるものです。コンポーネントのテストをするのフレームワークとしてJest、vue-test-utils 2を利用します。Vue2 系をターゲットにしているvue-test-utils 1とは一部 API が異なります。
なにをテストするのか
コンポーネントのテストは、詳細には触れずに入力と出力に着目してテストを行います。例えば、props として渡したデータが適切 DOM に描画されるかなどがテストケースとして挙げられます。
入力、出力はそれぞれ以下の分類があることが考えられます。
- 入力
- props
- 算出プロパティ
- フォーム入力、クリックなどのユーザーイベント
- store(Vuex など)の非同期アクション
- 出力
- DOM の描画
- emit
- store の状態の変更
テストを書いてみる
コンポーネントの実装
それでは実際にテストを書いていきます。 「Google でログイン・Twitter でログインなどのボタンをクリックした際にユーザー認証されてホーム画面へ遷移する」というシナリオを想定しています。下記がコンポーネントの実装です。
# LoginButtons.vue
<template>
<login-button @click="clickGoogle">Googleでログイン</login-button>
<login-button @click="clickTwitter">Twitterでログイン</login-button>
</template>
<script>
import { defineComponent } from 'vue'
import { useRouter } from 'vue-router'
import LoginButton from '@/components/molecules/LoginButton.vue'
import { signInWithGoogle, signInWithTwitter } from '@/composables/use-auth'
export default defineComponent({
components: {
LoginButton
},
setup () {
const router = useRouter()
const clickGoogle = async () => {
const user = await signInWithGoogle()
if (!user) return
router.push('/home')
}
const clickTwitter = async () => {
const user = await signInWithTwitter()
if (!user) return
router.push('/home')
}
return {
clickGoogle,
clickTwitter
}
}
})
</script>
テストファイルの作成
テストファイルは、test
フォルダ配下に配置することにします。test/components/LoginButtons.spec.ts
を作成します。
import { shallowMount } from '@vue/test-utils'
import LoginButtons from '@/components/organism/LoginButtons.vue'
import LoignButton from '@/components/molecules/LoginButton.vue'
import { signInWithGoogle } from '@/composables/use-auth'
import flushPromises from 'flush-promises'
const mockPush = jest.fn()
jest.mock('vue-router', () => ({
useRouter: () => {
return {
push: mockPush
}
}
}))
let mockError: boolean
jest.mock('@/composables/use-auth', () => ({
signInWithGoogle: jest.fn(() => {
if (mockError) {
return Promise.resolve(null)
}
const user = {
uid: 'uid',
displayName: 'name',
photoURL: 'photo'
}
return Promise.resolve(user)
})
}))
describe('LoginButtons.vue', () => {
const wrapper = shallowMount(LoginButtons)
const button = wrapper.findAllComponents(LoignButton)
test('Googleでログインボタンを押すと、signInWithGoogleが呼ばれホームページ遷移する', async () => {
button[0].trigger('click')
await flushPromises()
expect(signInWithGoogle).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/home')
})
test('Googeleでログインボタンを押した際に、ログインに失敗した場合ページ遷移はしない', async () => {
mockError = true
button[0].trigger('click')
await flushPromises()
expect(mockPush).toHaveBeenCalledTimes(0)
})
})
ざっとこのようなテストになるはずです。詳しく見ていきましょう。
外部モジュールをモック化する
実際に認証をする処理など副作用が発生する関数は、テストしづらくなってしますのですべてモック化します。またモック化をする際に処理を jest.fn()
に置き換えることでその関数がどのように呼ばれたのかをテストできます。
const mockPush = jest.fn()
jest.mock('vue-router', () => ({
useRouter: () => {
return {
push: mockPush
}
}
}))
let mockError: boolean
jest.mock('@/composables/use-auth', () => ({
signInWithGoogle: jest.fn(() => {
if (mockError) {
return Promise.resolve(null)
}
const user = {
uid: 'uid',
displayName: 'name',
photoURL: 'photo'
}
return Promise.resolve(user)
})
}))
jest.mock()
はモック化したいモジュールを第一引数に渡します。signInWithGoogle
をモック化する際には、mockError
変数によって成功時、失敗時をテストできるようにします。
コンポーネントをレンダリングして要素を取得する
コンポーネントをレンダリングするためには、mount
または shallowMount
を使います。mount
はコンポーネントのすべての要素をレンダリングしますが、shallowMount
は子コンポーネントをスタブ化してレンダリングします。
mount
・shallowMount
はレンダリングしたいコンポーネントを引数に受け取り、wrapperオブジェクトを返します。
レンダリングされた要素を取得するには、wrapper オブジェクトの find
・findAll
・findComponent
・findAllComponents
を利用します。
find
・findAll
は取得したい要素を querySelector で指定します。
findComponent
・findAllComponents
はコンポーネントインスタンスにより要素を検索します。
All
という名前がついているメソッドは、見つかった要素すべてを返します。
const wrapper = shallowMount(LoginButtons)
const buttons = wrapper.find(LoignButton)
ボタンクリックを発生させる
取得した要素のイベントを発生させるためには、trigger
メソッドを利用します。trigger
メソッドの引数には発生させたいイベント名を指定します。
Google でログインボタンはたしか 1 つ目の要素でしたので、buttons[0]
で要素を取得しています。
test('Googleでログインボタンを押すと、signInWithGoogleが呼ばれホームページ遷移する', async () => {
mockError = false
buttons[0].trigger('click')
await flushPromises()
expect(signInWithGoogle).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/home')
})
Google でログインボタンをクリックした際に発火する clickGoogle
関数は、非同期に実行されます。後続のアサーションまでにプロミスが解決していない場合には、テストが失敗してしまう可能性があります。
そのため、flush-promisesを利用して要素をクリックしてからすべてのプロミスをすぐに解決させます。
ここでは、signInWithGoogle
が呼ばれて、router.push()
が /home
という引数で呼ばれていることをテストしています。
終わりに
ここでは、クリックイベントの処理という簡単なテストの実行方法のみを取り上げました。props を与える、DOM の変更をテストする、emit の発生をテストするなど他にもさまざまなユースケースが考えられます。そのような要求に対してもわりと簡単にテストを実行できるので、リンクを参考に実施してみてください。