Vue3 + Vitest によるUT向けに Composable をモックするための Composable を書く

## はじめに
vue アプリケーションを作成する場合、状態管理やその関連処理をカプセル化するためにcomposableを使用することは多いと思います。

そのような composable を使用する側ソースのユニットテストを作成する際、毎回 composable をモックするのは無駄が大きいと思ったので、composable をモックするための composable を作成しておいて再利用可能にしようと思いました。

これはその思いつきの記録です。

## 内容

### モック対象の composable を作成する

composableのサンプルにはVue.jsガイドのuseFetchを流用します。

https://ja.vuejs.org/guide/reusability/composables.html#accepting-reactive-state

想定するテストを行えるようにするため、今回は、fetchDataがリアクティブで起動しない設計に作り変えておきます。

```typescript: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が実行される

```vuejs: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として用意することで再利用可能とします。
モック後の関数や、状態・投入引数の監視用変数を返却して、テストで利用できるようにしておきます。

```typescript: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が想定通りであるか

```typescript
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回
  })
})
```