Composable Testing
20.3.1. Composable Unit Testing
Composables must be executed within a Vue component context. Use a wrapper function.
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('should increment count when increment is called', () => {
const [result] = withSetup(() => useCounter())
result.increment()
expect(result.count.value).toBe(1)
})
})- Pure functions that do not depend on the Vue context may be tested directly without the wrapper.
20.3.2. Mocking External Dependencies
External dependencies such as APIs and Router must be mocked using 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('should fetch the user list', async () => {
vi.mocked(fetchUsers).mockResolvedValue([{ id: 1 }])
const [result] = withSetup(() => useUserList())
await result.load()
expect(result.users.value).toHaveLength(1)
})
})- Use
vi.clearAllMocks()inbeforeEachto reset state between tests.
20.3.3. Asynchronous Composables
For asynchronous logic, use flushPromises to resolve Promises before verifying state.
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('should set data correctly', async () => {
vi.mocked(fetchItems).mockResolvedValue([{ id: 1 }])
const [result] = withSetup(() => useFetchData())
await flushPromises()
expect(result.loading.value).toBe(false)
})
it('should set error on failure', async () => {
vi.mocked(fetchItems).mockRejectedValue(new Error('error'))
const [result] = withSetup(() => useFetchData())
await flushPromises()
expect(result.error.value).toBe('error')
})
})20.3.4. Timer Testing
For debounce, throttle, and similar functionality, use 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('should execute the search after 300ms', () => {
const [result] = withSetup(() => useDebouncedSearch())
const spy = vi.spyOn(result, 'executeSearch')
result.updateQuery('test')
vi.advanceTimersByTime(300)
expect(spy).toHaveBeenCalledOnce()
})
})- Activate fake timers in
beforeEachand restore them withvi.useRealTimers()inafterEach.