Skip to content

How to Test Vue Composables: A Comprehensive Guide with Vitest

Updated: at 

Introduction

Hello, everyone; in this blog post, I want to help you better understand how to test a composable in Vue. Nowadays, much of our business logic or UI logic is often encapsulated in composables, so I think it’s important to understand how to test them.

Definitions

Before discussing the main topic, it’s important to understand some basic concepts regarding testing. This foundational knowledge will help clarify where testing Vue compostables fits into the broader landscape of software testing.

Composables

Composables in Vue are reusable composition functions that encapsulate and manage reactive states and logic. They allow a flexible way to organize and reuse code across components, enhancing modularity and maintainability.

Testing Pyramid

The Testing Pyramid is a conceptual metaphor that illustrates the ideal balance of different types of testing. It recommends a large base of unit tests, supplemented by a smaller set of integration tests and capped with an even smaller set of end-to-end tests. This structure ensures efficient and effective test coverage.

Unit Testing and How Testing a Composable Would Be a Unit Test

Unit testing refers to the practice of testing individual units of code in isolation. In the context of Vue, testing a composable is a form of unit testing. It involves rigorously verifying the functionality of these isolated, reusable code blocks, ensuring they function correctly without external dependencies.


Testing Composables

Composables in Vue are essentially functions, leveraging Vue’s reactivity system. Given this unique nature, we can categorize composables into different types. On one hand, there are Independent Composables, which can be tested directly due to their standalone nature. On the other hand, we have Dependent Composables, which only function correctly when integrated within a component.In the sections that follow, I’ll delve into these distinct types, provide examples for each, and guide you through effective testing strategies for both.


Independent Composables

An Independent Composable exclusively uses Vue’s Reactivity APIs. These composables operate independently of Vue component instances, making them straightforward to test.

Example & Testing Strategy

Here is an example of an independent composable that calculates the sum of two reactive values:

import {interface Ref<T = any, S = T>Ref, 
const computed: {
    <T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ComputedRef<T>;
    <T, S = T>(options: WritableComputedOptions<T, S>, debugOptions?: DebuggerOptions): WritableComputedRef<T, S>;
}
computed
, interface ComputedRef<T = any>ComputedRef} from 'vue'
function function useSum(a: Ref<number>, b: Ref<number>): ComputedRef<number>useSum(a: Ref<number, number>a: interface Ref<T = any, S = T>Ref<number>, b: Ref<number, number>b: interface Ref<T = any, S = T>Ref<number>): interface ComputedRef<T = any>ComputedRef<number> { return computed<number>(getter: ComputedGetter<number>, debugOptions?: DebuggerOptions): ComputedRef<number> (+1 overload)
Takes a getter function and returns a readonly reactive ref object for the returned value from the getter. It can also take an object with get and set functions to create a writable ref object.
@example```js // Creating a readonly computed ref: const count = ref(1) const plusOne = computed(() => count.value + 1) console.log(plusOne.value) // 2 plusOne.value++ // error ``` ```js // Creating a writable computed ref: const count = ref(1) const plusOne = computed({ get: () => count.value + 1, set: (val) => { count.value = val - 1 } }) plusOne.value = 1 console.log(count.value) // 0 ```@paramgetter - Function that produces the next value.@paramdebugOptions - For debugging. See {@link https://vuejs.org/guide/extras/reactivity-in-depth.html#computed-debugging}.@see{@link https://vuejs.org/api/reactivity-core.html#computed}
computed
(() => a: Ref<number, number>a.Ref<number, number>.value: numbervalue + b: Ref<number, number>b.Ref<number, number>.value: numbervalue)
}

To test this composable, you would directly invoke it and assert its returned state:

Test with Vitest:

describe("useSum", () => {
  it("correctly computes the sum of two numbers", () => {
    const const num1: anynum1 = ref(2);
    const const num2: anynum2 = ref(3);
    const const sum: anysum = useSum(const num1: anynum1, const num2: anynum2);

    expect(const sum: anysum.value).toBe(5);
  });
});

This test directly checks the functionality of useSum by passing reactive references and asserting the computed result.


Dependent Composables

Dependent Composables are distinguished by their reliance on Vue’s component instance. They often leverage features like lifecycle hooks or context for their operation. These composables are an integral part of a component and necessitate a distinct approach for testing, as opposed to Independent Composables.

Example & Usage

An exemplary Dependent Composable is useLocalStorage. This composable facilitates interaction with the browser’s localStorage and harnesses the onMounted lifecycle hook for initialization:

import {function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
,
const computed: {
    <T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ComputedRef<T>;
    <T, S = T>(options: WritableComputedOptions<T, S>, debugOptions?: DebuggerOptions): WritableComputedRef<T, S>;
}
computed
, const onMounted: CreateHook<any>onMounted, function watch<T, Immediate extends Readonly<boolean> = false>(source: WatchSource<T>, cb: WatchCallback<T, MaybeUndefined<T, Immediate>>, options?: WatchOptions<Immediate>): WatchHandle (+3 overloads)watch} from 'vue'
function
function useLocalStorage<T>(key: string, initialValue: T): {
    value: [T] extends [Ref<any, any>] ? IfAny<T, Ref<T, T>, T> : Ref<UnwrapRef<T>, T | UnwrapRef<...>>;
}
useLocalStorage
<
function (type parameter) T in useLocalStorage<T>(key: string, initialValue: T): {
    value: [T] extends [Ref<any, any>] ? IfAny<T, Ref<T, T>, T> : Ref<UnwrapRef<T>, T | UnwrapRef<...>>;
}
T
>(key: stringkey: string, initialValue: TinitialValue:
function (type parameter) T in useLocalStorage<T>(key: string, initialValue: T): {
    value: [T] extends [Ref<any, any>] ? IfAny<T, Ref<T, T>, T> : Ref<UnwrapRef<T>, T | UnwrapRef<...>>;
}
T
) {
const const value: [T] extends [Ref<any, any>] ? IfAny<T, Ref<T, T>, T> : Ref<UnwrapRef<T>, T | UnwrapRef<T>>value = ref<T>(value: T): [T] extends [Ref<any, any>] ? IfAny<T, Ref<T, T>, T> : Ref<UnwrapRef<T>, T | UnwrapRef<T>> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
@paramvalue - The object to wrap in the ref.@see{@link https://vuejs.org/api/reactivity-core.html#ref}
ref
<
function (type parameter) T in useLocalStorage<T>(key: string, initialValue: T): {
    value: [T] extends [Ref<any, any>] ? IfAny<T, Ref<T, T>, T> : Ref<UnwrapRef<T>, T | UnwrapRef<...>>;
}
T
>(initialValue: TinitialValue);
function function (local function) loadFromLocalStorage(): voidloadFromLocalStorage() { const const storedValue: string | nullstoredValue = var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag. Any modification of this data outside of the Web Storage API is not supported. Enable this API with the `--experimental-webstorage` CLI flag.
@sincev22.4.0
localStorage
.Storage.getItem(key: string): string | null
Returns the current value associated with the given key, or null if the given key does not exist. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/getItem)
getItem
(key: stringkey);
if (const storedValue: string | nullstoredValue !== null) { const value: Ref<any, any> | Ref<T, T> | Ref<UnwrapRef<T>, T | UnwrapRef<T>>value.Ref<T = any, S = T>.value: anyvalue = var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON
.JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): any
Converts a JavaScript Object Notation (JSON) string into an object.
@paramtext A valid JSON string.@paramreviver A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is.
parse
(const storedValue: stringstoredValue);
} } function onMounted(hook: any, target?: ComponentInternalInstance | null): voidonMounted(function (local function) loadFromLocalStorage(): voidloadFromLocalStorage); watch<any, false>(source: WatchSource<any>, cb: WatchCallback<any, any>, options?: WatchOptions<false> | undefined): WatchHandle (+3 overloads)watch(const value: Ref<any, any> | Ref<T, T> | Ref<UnwrapRef<T>, T | UnwrapRef<T>>value, newValue: anynewValue => { var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag. Any modification of this data outside of the Web Storage API is not supported. Enable this API with the `--experimental-webstorage` CLI flag.
@sincev22.4.0
localStorage
.Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem)
setItem
(key: stringkey, var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON
.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
@paramvalue A JavaScript value, usually an object or array, to be converted.@paramreplacer A function that transforms the results.@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
stringify
(newValue: anynewValue));
}); return { value: [T] extends [Ref<any, any>] ? IfAny<T, Ref<T, T>, T> : Ref<UnwrapRef<T>, T | UnwrapRef<T>>value }; } export default
function useLocalStorage<T>(key: string, initialValue: T): {
    value: [T] extends [Ref<any, any>] ? IfAny<T, Ref<T, T>, T> : Ref<UnwrapRef<T>, T | UnwrapRef<...>>;
}
useLocalStorage
;

This composable can be utilised within a component, for instance, to create a persistent counter:

Counter Ui

<script setup lang="ts">
// ... script content ...
</script>

<template>
  <div: HTMLAttributes & ReservedPropsdiv>
    <h1: HTMLAttributes & ReservedPropsh1>Counter: {{ count }}</h1: HTMLAttributes & ReservedPropsh1>
    <button: ButtonHTMLAttributes & ReservedPropsbutton @onClick?: ((payload: MouseEvent) => void) | undefinedclick="increment">Increment</button: ButtonHTMLAttributes & ReservedPropsbutton>
  </div: HTMLAttributes & ReservedPropsdiv>
</template>

The primary benefit here is the seamless synchronization of the reactive count property with localStorage, ensuring persistence across sessions.

Testing Strategy

To effectively test useLocalStorage, especially considering the onMounted lifecycle, we initially face a challenge. Let’s start with a basic test setup:


describe("useLocalStorage", () => {
  it("should load the initialValue", () => {
    const { const value: Ref<string, string>value } = 
function useLocalStorage<string>(key: string, initialValue: string): {
    value: Ref<string, string>;
}
useLocalStorage
("testKey", "initValue");
expect(const value: Ref<string, string>value.Ref<string, string>.value: stringvalue).toBe("initValue"); }); it("should load from localStorage", async () => { var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag. Any modification of this data outside of the Web Storage API is not supported. Enable this API with the `--experimental-webstorage` CLI flag.
@sincev22.4.0
localStorage
.Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem)
setItem
("testKey", var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON
.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
@paramvalue A JavaScript value, usually an object or array, to be converted.@paramreplacer A function that transforms the results.@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
stringify
("fromStorage"));
const { const value: Ref<string, string>value } =
function useLocalStorage<string>(key: string, initialValue: string): {
    value: Ref<string, string>;
}
useLocalStorage
("testKey", "initialValue");
expect(const value: Ref<string, string>value.Ref<string, string>.value: stringvalue).toBe("fromStorage"); }); });

Here, the first test will pass, asserting that the composable initialises with the given initialValue. However, the second test, which expects the composable to load a pre-existing value from localStorage, fails. The challenge arises because the onMounted lifecycle hook is not triggered during testing. To address this, we need to refactor our composable or our test setup to simulate the component mounting process.


Enhancing Testing with the withSetup Helper Function

To facilitate easier testing of composables that rely on Vue’s lifecycle hooks, we’ve developed a higher-order function named withSetup. This utility allows us to create a Vue component context programmatically, focusing primarily on the setup lifecycle function where composables are typically used.

Introduction to withSetup

withSetup is designed to simulate a Vue component’s setup function, enabling us to test composables in an environment that closely mimics their real-world use. The function accepts a composable and returns both the composable’s result and a Vue app instance. This setup allows for comprehensive testing, including lifecycle and reactivity features.

import type { interface App<HostElement = any>App } from "vue";
import { const createApp: CreateAppFunction<Element>createApp } from "vue";

export function function withSetup<T>(composable: () => T): [T, App]withSetup<function (type parameter) T in withSetup<T>(composable: () => T): [T, App]T>(composable: () => Tcomposable: () => function (type parameter) T in withSetup<T>(composable: () => T): [T, App]T): [function (type parameter) T in withSetup<T>(composable: () => T): [T, App]T, interface App<HostElement = any>App] {
  let let result: Tresult: function (type parameter) T in withSetup<T>(composable: () => T): [T, App]T;
  const const app: App<Element>app = function createApp(rootComponent: Component, rootProps?: Data | null): App<Element>createApp({
    
ComponentOptionsBase<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions>.setup?: ((this: void, props: LooseRequired<any>, ctx: {
    attrs: Data;
    slots: Readonly<InternalSlots>;
    emit: ((event: unknown, ...args: any[]) => void) | ((event: string, ...args: any[]) => void);
    expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed) => void;
}) => any) | undefined
setup
() {
let result: Tresult = composable: () => Tcomposable(); return () => {}; }, }); const app: App<Element>app.App<Element>.mount(rootContainer: string | Element, isHydrate?: boolean, namespace?: boolean | ElementNamespace, vnode?: VNode): ComponentPublicInstancemount(var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.Document.createElement<"div">(tagName: "div", options?: ElementCreationOptions): HTMLDivElement (+2 overloads)
Creates an instance of the element for the specified tag.
@paramtagName The name of an element. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createElement)
createElement
("div"));
return [let result: Tresult, const app: App<Element>app]; }

In this implementation, withSetup mounts a minimal Vue app and executes the provided composable function during the setup phase. This approach allows us to capture and return the composable’s output alongside the app instance for further testing.

Utilizing withSetup in Tests

With withSetup, we can enhance our testing strategy for composables like useLocalStorage, ensuring they behave as expected even when they depend on lifecycle hooks:


it("should load the value from localStorage if it was set before", async () => {
  var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag. Any modification of this data outside of the Web Storage API is not supported. Enable this API with the `--experimental-webstorage` CLI flag.
@sincev22.4.0
localStorage
.Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem)
setItem
("testKey", var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON
.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
@paramvalue A JavaScript value, usually an object or array, to be converted.@paramreplacer A function that transforms the results.@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
stringify
("valueFromLocalStorage"));
const [
const result: {
    value: Ref<string, string>;
}
result
] =
function withSetup<{
    value: Ref<string, string>;
}>(composable: () => {
    value: Ref<string, string>;
}): [{
    value: Ref<string, string>;
}, App<any>]
withSetup
(() =>
function useLocalStorage<string>(key: string, initialValue: string): {
    value: Ref<string, string>;
}
useLocalStorage
("testKey", "testValue"));
expect(
const result: {
    value: Ref<string, string>;
}
result
.value: Ref<string, string>value.Ref<string, string>.value: stringvalue).toBe("valueFromLocalStorage");
});

This test demonstrates how withSetup enables the composable to execute as if it were part of a regular Vue component, ensuring the onMounted lifecycle hook is triggered as expected. Additionally, the robust TypeScript support enhances the development experience by providing clear type inference and error checking.


Testing Composables with Inject

Another common scenario is testing composables that rely on Vue’s dependency injection system using inject. These composables present unique challenges as they expect certain values to be provided by ancestor components. Let’s explore how to effectively test such composables.

Example Composable with Inject

Here’s an example of a composable that uses inject:

import type { type InjectionKey<T> = symbol & InjectionConstraint<T>InjectionKey } from 'vue'
import { function inject<T>(key: InjectionKey<T> | string): T | undefined (+2 overloads)inject } from 'vue'

export const const MessageKey: InjectionKey<string>MessageKey: type InjectionKey<T> = symbol & InjectionConstraint<T>InjectionKey<string> = 
var Symbol: SymbolConstructor
(description?: string | number) => symbol
Returns a new unique Symbol value.
@paramdescription Description of the new Symbol object.
Symbol
('message')
export function
function useMessage(): {
    message: string;
    getUpperCase: () => string;
    getReversed: () => string;
}
useMessage
() {
const const message: string | undefinedmessage = inject<string>(key: string | InjectionKey<string>): string | undefined (+2 overloads)inject(const MessageKey: InjectionKey<string>MessageKey) if (!const message: string | undefinedmessage) { throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Message must be provided')
} const const getUpperCase: () => stringgetUpperCase = () => const message: stringmessage.String.toUpperCase(): string
Converts all the alphabetic characters in a string to uppercase.
toUpperCase
()
const const getReversed: () => stringgetReversed = () => const message: stringmessage.String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)
Split a string into substrings using the specified separator and return them as an array.
@paramseparator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.@paramlimit A value used to limit the number of elements returned in the array.
split
('').Array<string>.reverse(): string[]
Reverses the elements in an array in place. This method mutates the array and returns a reference to the same array.
reverse
().Array<string>.join(separator?: string): string
Adds all the elements of an array into a string, separated by the specified separator string.
@paramseparator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma.
join
('')
return { message: stringmessage, getUpperCase: () => stringgetUpperCase, getReversed: () => stringgetReversed, } }

Creating a Test Helper

To test composables that use inject, we need a helper function that creates a testing environment with the necessary providers. Here’s a utility function that makes this possible:

import type { type InjectionKey<T> = symbol & InjectionConstraint<T>InjectionKey } from 'vue'
import { const createApp: CreateAppFunction<Element>createApp, 
function defineComponent<Props extends Record<string, any>, E extends EmitsOptions = {}, EE extends string = string, S extends SlotsType = {}>(setup: (props: Props, ctx: SetupContext<E, S>) => RenderFunction | Promise<RenderFunction>, options?: Pick<ComponentOptions, "name" | "inheritAttrs"> & {
    props?: (keyof Props)[];
    emits?: E | EE[];
    slots?: S;
}): DefineSetupFnComponent<Props, E, S> (+2 overloads)
defineComponent
, function h<K extends keyof HTMLElementTagNameMap>(type: K, children?: RawChildren): VNode (+22 overloads)h, function provide<T, K = string | number | InjectionKey<T>>(key: K, value: K extends InjectionKey<infer V> ? V : T): voidprovide } from 'vue'
type type InstanceType<V> = V extends new (...arg: any[]) => infer X ? X : neverInstanceType<function (type parameter) V in type InstanceType<V>V> = function (type parameter) V in type InstanceType<V>V extends { new (...arg: any[]arg: any[]): infer function (type parameter) XX } ? function (type parameter) XX : never type
type VM<V> = InstanceType<V> & {
    unmount: () => void;
}
VM
<function (type parameter) V in type VM<V>V> = type InstanceType<V> = V extends new (...arg: any[]) => infer X ? X : neverInstanceType<function (type parameter) V in type VM<V>V> & { unmount: () => voidunmount: () => void }
interface InjectionConfig { InjectionConfig.key: string | InjectionKey<any>key: type InjectionKey<T> = symbol & InjectionConstraint<T>InjectionKey<any> | string InjectionConfig.value: anyvalue: any } export function
function useInjectedSetup<TResult>(setup: () => TResult, injections?: InjectionConfig[]): TResult & {
    unmount: () => void;
}
useInjectedSetup
<
function (type parameter) TResult in useInjectedSetup<TResult>(setup: () => TResult, injections?: InjectionConfig[]): TResult & {
    unmount: () => void;
}
TResult
>(
setup: () => TResultsetup: () =>
function (type parameter) TResult in useInjectedSetup<TResult>(setup: () => TResult, injections?: InjectionConfig[]): TResult & {
    unmount: () => void;
}
TResult
,
injections: InjectionConfig[]injections: InjectionConfig[] = [], ):
function (type parameter) TResult in useInjectedSetup<TResult>(setup: () => TResult, injections?: InjectionConfig[]): TResult & {
    unmount: () => void;
}
TResult
& { unmount: () => voidunmount: () => void } {
let let result: TResultresult!:
function (type parameter) TResult in useInjectedSetup<TResult>(setup: () => TResult, injections?: InjectionConfig[]): TResult & {
    unmount: () => void;
}
TResult
const
const Comp: DefineComponent<{}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, {}, ComponentOptionsMixin, ... 13 more ..., any>
Comp
=
defineComponent<unknown, ComponentObjectPropsOptions<Data>, string, {}, {}, string, {}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, {}, {}, {}, string, ComponentProvideOptions, {}, {}, {}, any>(options: {
    ...;
} & ... 1 more ... & ThisType<...>): DefineComponent<...> (+2 overloads)
defineComponent
({
ComponentOptionsBase<ToResolvedProps<{}, {}>, () => VNode<RendererNode, RendererElement, { [key: string]: any; }>, {}, {}, {}, ComponentOptionsMixin, ... 10 more ..., ComponentProvideOptions>.setup?: ((this: void, props: LooseRequired<Readonly<{}> & Readonly<{}> & {}>, ctx: {
    attrs: Data;
    slots: Readonly<InternalSlots>;
    emit: (event: string, ...args: any[]) => void;
    expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed) => void;
}) => void | ... 2 more ... | Promise<...>) | undefined
setup
() {
let result: TResultresult = setup: () => TResultsetup() return () => h<"div">(type: "div", children?: RawChildren): VNode (+22 overloads)h('div') }, }) const
const Provider: DefineComponent<{}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, {}, ComponentOptionsMixin, ... 13 more ..., any>
Provider
=
defineComponent<unknown, ComponentObjectPropsOptions<Data>, string, {}, {}, string, {}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, {}, {}, {}, string, ComponentProvideOptions, {}, {}, {}, any>(options: {
    ...;
} & ... 1 more ... & ThisType<...>): DefineComponent<...> (+2 overloads)
defineComponent
({
ComponentOptionsBase<ToResolvedProps<{}, {}>, () => VNode<RendererNode, RendererElement, { [key: string]: any; }>, {}, {}, {}, ComponentOptionsMixin, ... 10 more ..., ComponentProvideOptions>.setup?: ((this: void, props: LooseRequired<Readonly<{}> & Readonly<{}> & {}>, ctx: {
    attrs: Data;
    slots: Readonly<InternalSlots>;
    emit: (event: string, ...args: any[]) => void;
    expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed) => void;
}) => void | ... 2 more ... | Promise<...>) | undefined
setup
() {
injections: InjectionConfig[]injections.Array<InjectionConfig>.forEach(callbackfn: (value: InjectionConfig, index: number, array: InjectionConfig[]) => void, thisArg?: any): void
Performs the specified action for each element in an array.
@paramcallbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
forEach
(({ key: string | InjectionKey<any>key, value: anyvalue }) => {
provide<any, string | InjectionKey<any>>(key: string | InjectionKey<any>, value: any): voidprovide(key: string | InjectionKey<any>key, value: anyvalue) }) return () => function h(type: Component, children?: RawChildren): VNode (+22 overloads)h(
const Comp: DefineComponent<{}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, {}, ComponentOptionsMixin, ... 13 more ..., any>
Comp
)
}, }) const
const mounted: VM<DefineComponent<{}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, {}, ComponentOptionsMixin, ... 13 more ..., any>>
mounted
=
function mount<DefineComponent<{}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, {}, ComponentOptionsMixin, ... 13 more ..., any>>(Comp: DefineComponent<...>): VM<...>
mount
(
const Provider: DefineComponent<{}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, {}, ComponentOptionsMixin, ... 13 more ..., any>
Provider
)
return { ...let result: TResultresult, unmount: () => voidunmount:
const mounted: VM<DefineComponent<{}, () => VNode<RendererNode, RendererElement, {
    [key: string]: any;
}>, {}, {}, {}, ComponentOptionsMixin, ... 13 more ..., any>>
mounted
.unmount: () => voidunmount,
} as
function (type parameter) TResult in useInjectedSetup<TResult>(setup: () => TResult, injections?: InjectionConfig[]): TResult & {
    unmount: () => void;
}
TResult
& { unmount: () => voidunmount: () => void }
} function function mount<V>(Comp: V): VM<V>mount<function (type parameter) V in mount<V>(Comp: V): VM<V>V>(type Comp: VComp: function (type parameter) V in mount<V>(Comp: V): VM<V>V) { const const el: HTMLDivElementel = var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)
document
.Document.createElement<"div">(tagName: "div", options?: ElementCreationOptions): HTMLDivElement (+2 overloads)
Creates an instance of the element for the specified tag.
@paramtagName The name of an element. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createElement)
createElement
('div')
const const app: App<Element>app = function createApp(rootComponent: Component, rootProps?: Data | null): App<Element>createApp(type Comp: VComp as any) const const unmount: () => voidunmount = () => const app: App<Element>app.App<Element>.unmount(): voidunmount() const const comp: VM<V>comp = const app: App<Element>app.App<Element>.mount(rootContainer: string | Element, isHydrate?: boolean, namespace?: boolean | ElementNamespace, vnode?: VNode): ComponentPublicInstancemount(const el: HTMLDivElementel) as any as
type VM<V> = InstanceType<V> & {
    unmount: () => void;
}
VM
<function (type parameter) V in mount<V>(Comp: V): VM<V>V>
const comp: VM<V>comp.unmount: () => voidunmount = const unmount: () => voidunmount return const comp: VM<V>comp }

Writing Tests

With our helper function in place, we can now write comprehensive tests for our inject-dependent composable:

import { import describedescribe, import expectexpect, import itit } from 'vitest'
import { import useInjectedSetupuseInjectedSetup } from '../helper'
import { import MessageKeyMessageKey, import useMessageuseMessage } from '../useMessage'

import describedescribe('useMessage', () => {
  import itit('should handle injected message', () => {
    const const wrapper: anywrapper = import useInjectedSetupuseInjectedSetup(
      () => import useMessageuseMessage(),
      [{ key: anykey: import MessageKeyMessageKey, value: stringvalue: 'hello world' }],
    )

    import expectexpect(const wrapper: anywrapper.message).toBe('hello world')
    import expectexpect(const wrapper: anywrapper.getUpperCase()).toBe('HELLO WORLD')
    import expectexpect(const wrapper: anywrapper.getReversed()).toBe('dlrow olleh')

    const wrapper: anywrapper.unmount()
  })

  import itit('should throw error when message is not provided', () => {
    import expectexpect(() => {
      import useInjectedSetupuseInjectedSetup(() => import useMessageuseMessage(), [])
    }).toThrow('Message must be provided')
  })
})

The useInjectedSetup helper creates a testing environment that:

  1. Simulates a component hierarchy
  2. Provides the necessary injection values
  3. Executes the composable in a proper Vue context
  4. Returns the composable’s result along with an unmount function

This approach allows us to:

Remember to always unmount the test component after each test to prevent memory leaks and ensure test isolation.


Summary

Independent Composables 🔓Dependent Composables 🔗
- ✅ can be tested directly- 🧪 need a component to test
- 🛠️ uses everything beside of lifecycles and provide / inject- 🔄 uses Lifecycles or Provide / Inject

In our exploration of testing Vue composables, we uncovered two distinct categories: Independent Composables and Dependent Composables. Independent Composables stand alone and can be tested akin to regular functions, showcasing straightforward testing procedures. Meanwhile, Dependent Composables, intricately tied to Vue’s component system and lifecycle hooks, require a more nuanced approach. For these, we learned the effectiveness of utilizing a helper function, such as withSetup, to simulate a component context, enabling comprehensive testing.

I hope this blog post has been insightful and useful in enhancing your understanding of testing Vue composables. I’m also keen to learn about your experiences and methods in testing composables within your projects. Your insights and approaches could provide valuable perspectives and contribute to the broader Vue community’s knowledge.

Related Posts