Skip to content

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() in beforeEach to 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 beforeEach and restore them with vi.useRealTimers() in afterEach.

TIENIPIA QUALIFIED STANDARD