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)
  })
})
  • 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()로 복원합니다.

TIENIPIA QUALIFIED STANDARD