vue-composable-testing

Vue Composable Testing Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "vue-composable-testing" with this command: npx skills add alexanderop/workouttracker/alexanderop-workouttracker-vue-composable-testing

Vue Composable Testing Guide

Test composables based on their dependency type: Independent (direct testing) or Dependent (requires component context).

Quick Classification

Type Characteristics Testing Approach

Independent Uses only reactivity APIs (ref, computed, watch) Test directly like functions

Dependent Uses lifecycle hooks (onMounted) or inject Use withSetup or useInjectedSetup helpers

Testing Independent Composables

Independent composables use only Vue's reactivity system without lifecycle hooks or dependency injection.

// useSum.ts - Independent composable import type { ComputedRef, Ref } from 'vue' import { computed } from 'vue'

export function useSum(a: Ref<number>, b: Ref<number>): ComputedRef<number> { return computed(() => a.value + b.value) }

import { describe, expect, it } from 'vitest' // useSum.spec.ts - Direct testing import { ref } from 'vue' import { useSum } from '../useSum'

describe('useSum', () => { it('computes sum of two numbers', () => { const num1 = ref(2) const num2 = ref(3) const sum = useSum(num1, num2)

expect(sum.value).toBe(5)

})

it('reacts to value changes', () => { const num1 = ref(1) const num2 = ref(1) const sum = useSum(num1, num2)

num1.value = 10
expect(sum.value).toBe(11)

}) })

Testing Dependent Composables

With Lifecycle Hooks

Composables using onMounted , onUnmounted , etc. require a component context. Use the withSetup helper.

// useLocalStorage.ts - Uses onMounted import { onMounted, ref, watch } from 'vue'

export function useLocalStorage<TValue>(key: string, initialValue: TValue) { const value = ref<TValue>(initialValue)

onMounted(() => { const stored = localStorage.getItem(key) if (stored !== null) { value.value = JSON.parse(stored) } })

watch(value, (newValue) => { localStorage.setItem(key, JSON.stringify(newValue)) })

return { value } }

// useLocalStorage.spec.ts - Using withSetup import { beforeEach, describe, expect, it } from 'vitest' import { useLocalStorage } from '../useLocalStorage' import { withSetup } from './helpers/withSetup'

describe('useLocalStorage', () => { beforeEach(() => { localStorage.clear() })

it('loads initial value', () => { const [result] = withSetup(() => useLocalStorage('key', 'initial')) expect(result.value.value).toBe('initial') })

it('loads value from localStorage on mount', () => { localStorage.setItem('key', JSON.stringify('stored')) const [result] = withSetup(() => useLocalStorage('key', 'initial')) expect(result.value.value).toBe('stored') })

it('persists changes to localStorage', () => { const [result] = withSetup(() => useLocalStorage('key', 'initial')) result.value.value = 'updated' expect(JSON.parse(localStorage.getItem('key')!)).toBe('updated') }) })

With Inject

Composables using inject require provided values. Use the useInjectedSetup helper.

// useMessage.ts - Uses inject import type { InjectionKey } from 'vue' import { inject } from 'vue'

export const MessageKey: InjectionKey<string> = Symbol('message')

export function useMessage() { const message = inject(MessageKey)

if (!message) { throw new Error('Message must be provided') }

return { message, getUpperCase: () => message.toUpperCase(), getReversed: () => message.split('').reverse().join(''), } }

// useMessage.spec.ts - Using useInjectedSetup import { describe, expect, it } from 'vitest' import { MessageKey, useMessage } from '../useMessage' import { useInjectedSetup } from './helpers/useInjectedSetup'

describe('useMessage', () => { it('uses injected message', () => { const result = useInjectedSetup( () => useMessage(), [{ key: MessageKey, value: 'hello world' }] )

expect(result.message).toBe('hello world')
expect(result.getUpperCase()).toBe('HELLO WORLD')
expect(result.getReversed()).toBe('dlrow olleh')

result.unmount()

})

it('throws when message not provided', () => { expect(() => { useInjectedSetup(() => useMessage(), []) }).toThrow('Message must be provided') }) })

Helper Functions

Create these helpers in your test utilities. See references/test-helpers.md for complete implementations.

withSetup

Mounts a minimal Vue app to trigger lifecycle hooks:

import type { App } from 'vue' import { createApp } from 'vue'

export function withSetup<TResult>(composable: () => TResult): [TResult, App] { let result: TResult const app = createApp({ setup() { result = composable() return () => {} }, }) app.mount(document.createElement('div')) return [result!, app] }

useInjectedSetup

Creates a provider component for inject-dependent composables:

import type { InjectionKey } from 'vue' import { createApp, defineComponent, h, provide } from 'vue'

interface InjectionConfig { key: InjectionKey<unknown> | string value: unknown }

export function useInjectedSetup<TResult>( setup: () => TResult, injections: ReadonlyArray<InjectionConfig> = [] ): TResult & { unmount: () => void } { let result!: TResult

const Comp = defineComponent({ setup() { result = setup() return () => h('div') }, })

const Provider = defineComponent({ setup() { injections.forEach(({ key, value }) => { provide(key, value) }) return () => h(Comp) }, })

const el = document.createElement('div') const app = createApp(Provider) app.mount(el)

return { ...result, unmount: () => app.unmount(), } }

Testing Patterns

Async Composables

describe('useAsyncData', () => { it('handles async operations', async () => { const [result] = withSetup(() => useAsyncData())

expect(result.isLoading.value).toBe(true)

await result.fetch()

expect(result.isLoading.value).toBe(false)
expect(result.data.value).toBeDefined()

}) })

Error States

describe('useFetch', () => { it('captures errors', async () => { vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('Network error'))

const [result] = withSetup(() => useFetch('/api/data'))
await result.execute()

expect(result.error.value).toBeInstanceOf(Error)
expect(result.error.value?.message).toBe('Network error')

}) })

Cleanup

describe('useInterval', () => { it('cleans up on unmount', () => { const clearSpy = vi.spyOn(globalThis, 'clearInterval')

const [, app] = withSetup(() => useInterval(() => {}, 1000))
app.unmount()

expect(clearSpy).toHaveBeenCalled()

}) })

Best Practices

  • Classify first - Determine if composable is independent or dependent before writing tests

  • Always unmount - Call app.unmount() or result.unmount() to prevent memory leaks

  • Test reactivity - Verify computed values update when dependencies change

  • Mock external APIs - Use vi.spyOn or vi.mock for fetch, localStorage, etc.

  • Test error paths - Verify composables expose errors correctly

  • Clear state - Use beforeEach to reset mocks and external state like localStorage

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

vue-composables

No summary provided by upstream source.

Repository SourceNeeds Review
General

product-planning

No summary provided by upstream source.

Repository SourceNeeds Review
General

vitest-mocking

No summary provided by upstream source.

Repository SourceNeeds Review