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)
})
})beforeEach에서vi.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. 타이머 테스트
debounce, throttle 등은 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에서 활성화하고afterEach에서vi.useRealTimers()로 복원합니다.