Vue3 コンポーネントのテスト
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の発生をテストするなど他にも様々なユースケースが考えられます。そのような要求に対してもわりと簡単にテストを実行することができるので、リンクを参考に実施してみてください。