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