Skip to content

Composable テスト

20.3.1. Composable 単体テスト

Composable は Vue コンポーネントコンテキスト内で実行されなければなりません。ラッパー関数を使用します。

typescript
// test-utils/withSetup.ts
import { createApp, type App } from 'vue'
export function withSetup<T>(composable: () => T): [T, App] {
  let result!: T
  const app = createApp({
    setup() { result = composable(); return () => {} },
  })
  app.mount(document.createElement('div'))
  return [result, app]
}
typescript
describe('useCounter', () => {
  it('increment 呼び出し時に count が増加しなければなりません', () => {
    const [result] = withSetup(() => useCounter())
    result.increment()
    expect(result.count.value).toBe(1)
  })
})
  • Vue コンテキストに依存しない純粋関数はラッパーなしで直接テストします。

20.3.2. 外部依存関係のモッキング

API、Router などの外部依存関係は vi.mock() でモッキングします。

typescript
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { withSetup } from '@/test-utils/withSetup'
import { useUserList } from '../useUserList'
import { fetchUsers } from '@/api/user'
vi.mock('@/api/user', () => ({ fetchUsers: vi.fn() }))
vi.mock('vue-router', () => ({
  useRouter: vi.fn(() => ({ push: vi.fn() })),
  useRoute: vi.fn(() => ({ params: {}, query: {} })),
}))

describe('useUserList', () => {
  beforeEach(() => { vi.clearAllMocks() })
  it('ユーザー一覧を取得しなければなりません', async () => {
    vi.mocked(fetchUsers).mockResolvedValue([{ id: 1 }])
    const [result] = withSetup(() => useUserList())
    await result.load()
    expect(result.users.value).toHaveLength(1)
  })
})
  • beforeEachvi.clearAllMocks() を使用してテスト間の状態を初期化します。

20.3.3. 非同期 Composable

非同期ロジックは flushPromises で Promise 解決後に状態を検証します。

typescript
import { describe, it, expect, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { withSetup } from '@/test-utils/withSetup'
import { useFetchData } from '../useFetchData'
import { fetchItems } from '@/api/item'
vi.mock('@/api/item', () => ({ fetchItems: vi.fn() }))
describe('useFetchData', () => {
  it('データを正常に設定しなければなりません', async () => {
    vi.mocked(fetchItems).mockResolvedValue([{ id: 1 }])
    const [result] = withSetup(() => useFetchData())
    await flushPromises()
    expect(result.loading.value).toBe(false)
  })
  it('失敗時にエラーを設定しなければなりません', async () => {
    vi.mocked(fetchItems).mockRejectedValue(new Error('エラー'))
    const [result] = withSetup(() => useFetchData())
    await flushPromises()
    expect(result.error.value).toBe('エラー')
  })
})

20.3.4. タイマーテスト

debouncethrottle などは vi.useFakeTimers() を使用します。

typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { withSetup } from '@/test-utils/withSetup'
import { useDebouncedSearch } from '../useDebouncedSearch'
describe('useDebouncedSearch', () => {
  beforeEach(() => { vi.useFakeTimers() })
  afterEach(() => { vi.useRealTimers() })
  it('300ms 後に検索を実行しなければなりません', () => {
    const [result] = withSetup(() => useDebouncedSearch())
    const spy = vi.spyOn(result, 'executeSearch')
    result.updateQuery('テスト')
    vi.advanceTimersByTime(300)
    expect(spy).toHaveBeenCalledOnce()
  })
})
  • beforeEach で有効化し、afterEachvi.useRealTimers() により復元します。

TIENIPIA QUALIFIED STANDARD