Mocking
Replace a child, a composable, the router, a store or fetch with something predictable, and keep the test about your component.
vi.mock('@/composables/useMessages', () => ({ useMessages: () => ({ /* refs */ }) }))A mock is a stand-in. You reach for one when a real dependency is slow, unavailable in jsdom, or simply not the thing under test. The goal is a test that fails only when your component is wrong.
Children
The global.stubs mounting option replaces a child component with a placeholder. stubs: ['AvatarBadge'] is the array shorthand for stubs: { AvatarBadge: true } and they do the same thing. Pass a small component instead of true when the test needs the child to render something.
Modules
vi.mock(path, factory) replaces a whole module for the file it appears in. This is how you mock a composable: return an object with the same exported names, where the refs hold the state the test wants. The same call mocks vue-router, so useRouter() hands your component a push you can assert on without a real router.
Stores
Pinia ships a test helper in a separate package, @pinia/testing. createTestingPinia() installs a Pinia whose actions are replaced by spies, so they record calls instead of running, and whose starting state you can set with initialState. Pass createSpy: vi.fn unless you have Vitest globals enabled.
The network
Never let a unit test make a request. vi.stubGlobal('fetch', vi.fn()) swaps the global for one you control, and vi.unstubAllGlobals() puts it back.
Example
// src/components/__tests__/InboxPanel.spec.ts
import { describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { mount } from '@vue/test-utils'
import InboxPanel from '../InboxPanel.vue'
vi.mock('@/composables/useMessages', () => ({
useMessages: () => ({
messages: ref([
{ id: 1, subject: 'Invoice' },
{ id: 2, subject: 'Receipt' },
]),
pending: ref(false),
error: ref<Error | null>(null),
}),
}))
describe('InboxPanel', () => {
it('renders one row per message the composable returns', () => {
const wrapper = mount(InboxPanel, {
global: { stubs: { MessageRow: true } },
})
expect(wrapper.findAll('message-row-stub')).toHaveLength(2)
expect(wrapper.get('[data-testid="count"]').text()).toBe('2 messages')
})
})When to use it
- Test a page component without the API call its data composable makes.
- Assert a component asked the router to navigate, without mounting the whole route tree.
- Check a component dispatches a store action without running it.
- Keep a child that renders a map or a chart out of a unit test.
More examples
Faking the router
Mocking the vue-router module gives the component a push it does not actually navigate with, which is usually what a unit test wants: the assertion is that the component asked to go somewhere. Because the factory replaces the whole module, every export the component uses has to appear in it, which is why useRoute is there too. When you do want real navigation, mount with global.plugins: [router] using createRouter and createMemoryHistory, then await router.isReady() before asserting.
// src/components/__tests__/AccountMenu.spec.ts
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { useRouter } from 'vue-router'
import AccountMenu from '../AccountMenu.vue'
vi.mock('vue-router', () => ({
useRouter: vi.fn(),
useRoute: vi.fn(() => ({ params: {}, query: {} })),
}))
describe('AccountMenu', () => {
const push = vi.fn()
beforeEach(() => {
push.mockClear()
vi.mocked(useRouter).mockReturnValue(
{ push } as unknown as ReturnType<typeof useRouter>,
)
})
it('navigates to billing when that item is chosen', async () => {
const wrapper = mount(AccountMenu, { props: { email: '[email protected]' } })
await wrapper.get('[data-testid="billing"]').trigger('click')
expect(push).toHaveBeenCalledWith({ name: 'billing' })
})
})A store with spied actions
createTestingPinia stubs every action by default, so cart.clear records the call and the state is left alone, which is why items still has one entry at the end. initialState is keyed by store id, so cart here matches defineStore('cart', ...). Pass stubActions: false when the test is about what the action actually does. Install the helper separately: npm install -D @pinia/testing.
// src/components/__tests__/CartSummary.spec.ts
import { expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { useCartStore } from '@/stores/cart'
import CartSummary from '../CartSummary.vue'
it('shows the count and asks the store to clear', async () => {
const wrapper = mount(CartSummary, {
global: {
plugins: [
createTestingPinia({
createSpy: vi.fn,
initialState: {
cart: { items: [{ id: 1, quantity: 2 }] },
},
}),
],
},
})
const cart = useCartStore()
expect(wrapper.text()).toContain('1 item')
await wrapper.get('[data-testid="clear"]').trigger('click')
expect(cart.clear).toHaveBeenCalledTimes(1)
expect(cart.items).toHaveLength(1)
})A fake fetch
Returning a real Response object means the composable's own res.ok and res.json() calls behave normally, so you are testing your code rather than a hand-rolled fake. Holding the promise lets the test assert the pending flag before resolution, which is the part users actually see. flushPromises, exported by @vue/test-utils, drains anything queued after the await. For a whole suite of endpoints a request-interception library such as MSW scales better than stubbing fetch per test.
// src/composables/__tests__/useOrders.spec.ts
import { afterEach, expect, it, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import { useOrders } from '../useOrders'
afterEach(() => {
vi.unstubAllGlobals()
})
it('exposes the orders the endpoint returned', async () => {
const body = JSON.stringify({ orders: [{ id: 'a1', total: 20 }] })
const fetchMock = vi.fn(() =>
Promise.resolve(
new Response(body, {
status: 200,
headers: { 'content-type': 'application/json' },
}),
),
)
vi.stubGlobal('fetch', fetchMock)
const { orders, pending, load } = useOrders()
const done = load()
expect(pending.value).toBe(true)
await done
await flushPromises()
expect(fetchMock).toHaveBeenCalledWith('/api/orders', expect.anything())
expect(orders.value).toEqual([{ id: 'a1', total: 20 }])
// Output: [ { id: 'a1', total: 20 } ]
})
Discussion