はじめに
vue アプリケーションを作成する場合、状態管理やその関連処理をカプセル化するためにcomposableを使用することは多いと思います。
そのような composable を使用する側ソースのユニットテストを作成する際、毎回 composable をモックするのは無駄が大きいと思ったので、composable をモックするための composable を作成しておいて再利用可能にしようと思いました。
これはその思いつきの記録です。
内容
モック対象の composable を作成する
composableのサンプルにはVue.jsガイドのuseFetchを流用します。
想定するテストを行えるようにするため、今回は、fetchDataがリアクティブで起動しない設計に作り変えておきます。
fetch.ts
import { ref } from 'vue'
import type { Ref } from 'vue'
export const useFetch = (url: Ref<string | undefined>) => {
const data = ref<string | null>(null)
const error = ref<string | null>(null)
const fetchData = async () => {
if (!url.value) return
data.value = null
error.value = null
await fetch(url.value)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err.message))
}
return { fetchData, data, error }
}
テスト対象ソースを作成する
ここでは、テスト対象ソースはcomponentとし、以下の機能を持つことを想定します。
- DOMノード生成後に、fetchが実行される
- ChangeUrlボタンクリック時に、変更されたurlでfetchが実行される
MyComponent.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useFetch } from './fetch'
const url = ref<string>()
const { fetchData, data } = useFetch(url)
const changeUrl = () => {
url.value = '/new-url'
fetchData()
}
onMounted(() => {
url.value = '/initial-url'
fetchData()
})
</script>
<template>
<div>
<div>
{{ data ?? '' }}
</div>
<button @click="changeUrl">
ChangeUrl
</button>
</div>
</template>
モック Composable を作成する
fetch.ts内のメソッドのモック処理を、composableとして用意することで再利用可能とします。
モック後の関数や、状態・投入引数の監視用変数を返却して、テストで利用できるようにしておきます。
fetch.mock.ts
import { ref, watch } from 'vue'
import * as fetch from './fetch'
export const useFetchMock = (dummyData: string | null) => {
const data = ref<string | null>(null)
const error = ref<string | null>(null)
const url = ref<string | undefined>()
const fetchDataSpy = vi.fn(() => {
data.value = dummyData
return Promise.resolve()
})
vi.spyOn(fetch, 'useFetch').mockImplementation((param) => {
watch(param, () => {
url.value = param.value
})
return {
fetchData: fetchDataSpy,
data,
error,
}
})
return {
fetchDataSpy,
data,
url,
}
}
テストソースを作成する
先に作成した fetch.mock.tsを使用 することで、以下のような観点のテストができるようになりました。
- fetchDataの起動回数が想定通りであるか
- fetchDataの起動時のurlが想定通りであるか
import { describe, it } from 'vitest'
import { mount } from '@vue/test-utils'
import { useFetchMock } from './fetch.mock'
import MyComponent from './MyComponent.vue'
import type { VueWrapper } from '@vue/test-utils'
describe('test MyComponent.vue', () => {
const { fetchDataSpy, url } = useFetchMock('result data') // ここでダミーデータを渡す
let wrapper: VueWrapper<typeof MyComponent>
beforeEach(() => {
wrapper = mount(MyComponent)
})
afterEach(() => {
fetchDataSpy.mockClear()
})
it('初期表示時にfetchが実行され、dataが表示される', () => {
expect(url.value).toBe('/initial-url')
expect(wrapper.text()).toMatch(/result data/)
expect(fetchDataSpy).toHaveBeenCalledOnce()
})
it('ChangeUrlボタンクリックによって新しいurlでfetchが実行される', async () => {
const button = wrapper.find('button')
await button.trigger('click')
expect(url.value).toBe('/new-url')
expect(wrapper.text()).toMatch(/result data/)
expect(fetchDataSpy).toHaveBeenCalledTimes(2) // 計2回
})
})