🎯

vue-composable-testing

🎯Skill

from alexanderop/workouttracker

VibeIndex|
What it does

vue-composable-testing skill from alexanderop/workouttracker

vue-composable-testing

Installation

Install skill:
npx skills add https://github.com/alexanderop/workouttracker --skill vue-composable-testing
6
Last UpdatedJan 26, 2026

Skill Details

SKILL.md

Test Vue 3 composables with Vitest following best practices. Use when writing tests for composables, creating test helpers for Vue reactivity, or when asked to "test a composable", "write tests for use*", or "help test Vue composition functions". Covers independent composables (direct testing) and dependent composables (lifecycle/inject testing with helpers).

Overview

# 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.

```ts

// useSum.ts - Independent composable

import type { ComputedRef, Ref } from 'vue'

import { computed } from 'vue'

export function useSum(a: Ref, b: Ref): ComputedRef {

return computed(() => a.value + b.value)

}

```

```ts

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.

```ts

// useLocalStorage.ts - Uses onMounted

import { onMounted, ref, watch } from 'vue'

export function useLocalStorage(key: string, initialValue: TValue) {

const value = ref(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 }

}

```

```ts

// 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.

```ts

// useMessage.ts - Uses inject

import type { InjectionKey } from 'vue'

import { inject } from 'vue'

export const MessageKey: InjectionKey = 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(''),

}

}

```

```ts

// 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:

```ts

import type { App } from 'vue'

import { createApp } from 'vue'

export function withSetup(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:

```ts

import type { InjectionKey } from 'vue'

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

interface InjectionConfig {

key: InjectionKey | string

value: unknown

}

export function useInjectedSetup(

setup: () => TResult,

injections: ReadonlyArray = []

): 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

```ts

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

```ts

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

```ts

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

  1. Classify first - Determine if composable is independent or dependent before writing tests
  2. Always unmount - Call app.unmount() or result.unmount() to prevent memory leaks
  3. Test reactivity - Verify computed values update when dependencies change
  4. Mock external APIs - Use vi.spyOn or vi.mock for fetch, localStorage, etc.
  5. Test error paths - Verify composables expose errors correctly
  6. Clear state - Use beforeEach to reset mocks and external state like localStorage