Mocking API Calls in Vue Tests with Vitest
Vue components rarely live in isolation. Most of them fetch data, submit forms, or poll endpoints. When you test those components without controlling the network, your tests become slow, flaky, and dependent on external services. Mocking API calls in Vue tests helps eliminate those problems by letting you control the responses your components receive.
This article covers two practical strategies for Vitest Vue API mocking: replacing the API module directly with vi.mock, and intercepting requests at the HTTP layer using Mock Service Worker (MSW). You’ll also learn the Vue-specific async handling that makes both approaches work reliably.
Key Takeaways
- Real network calls in tests produce flaky, slow, non-deterministic results — mocking gives you full control over responses.
vi.mockreplaces the API module at the import level, making it ideal for fast, isolated unit tests.- MSW intercepts requests at the network layer, letting your actual fetch or axios logic execute for more realistic integration tests.
flushPromises()from@vue/test-utilsis commonly needed when components await asynchronous requests before updating the DOM.
Why You Must Mock API Calls in Vue Component Testing with Vitest
Real network calls in tests produce non-deterministic results. The same test can pass locally and fail in CI simply because an external API is slow or unavailable. Mocking gives you control over what the network returns, so your tests stay fast and predictable.
Strategy 1: Mocking the API Module with vi.mock
The most direct approach is replacing the module responsible for HTTP calls before your component ever sees it.
// quote.service.ts
export async function fetchQuote() {
const response = await fetch('https://api.example.com/quotes/random')
return response.json()
}
// QuoteCard.test.ts
import { mount, flushPromises } from '@vue/test-utils'
import { vi, test, expect } from 'vitest'
import { fetchQuote } from './quote.service'
import QuoteCard from './QuoteCard.vue'
// vi.mock is hoisted above imports automatically
vi.mock('./quote.service')
test('displays a quote after mounting', async () => {
vi.mocked(fetchQuote).mockResolvedValue({
id: 1,
quote: 'Test quote',
author: 'Tester',
})
const wrapper = mount(QuoteCard)
// Wait for all pending promises and DOM updates to settle
await flushPromises()
expect(wrapper.text()).toContain('Test quote')
})
One critical detail: vi.mock is hoisted to the top of the file by Vitest, so it runs before any imports. This is intentional behavior documented in the Vitest mocking API. It means you can safely import the real function and still receive the mocked version inside your test.
The flushPromises() call is often required in this pattern. Vue components that fetch data on mount update their DOM asynchronously. Utilities like flushPromises ensure that pending promises resolve before assertions run, preventing tests from asserting against loading states.
When to use this approach: Unit testing a specific component or service in isolation, where you want tight control over return values and call counts.
Discover how at OpenReplay.com.
Strategy 2: MSW with Vitest Vue Tests for HTTP-Layer Mocking
Mock Service Worker intercepts requests at the network level rather than replacing JavaScript modules. This makes your tests more realistic because the actual fetch or axios call still executes — MSW just intercepts it before it leaves the process.
Install MSW and set it up for a Node environment (the default for Vitest):
npm install -D msw
// test/server.ts
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
export const server = setupServer(
http.get('https://api.example.com/quotes/random', () => {
return HttpResponse.json({ id: 1, quote: 'MSW quote', author: 'MSW' })
})
)
// QuoteCard.test.ts
import { mount, flushPromises } from '@vue/test-utils'
import { test, expect, beforeAll, afterEach, afterAll } from 'vitest'
import { server } from './test/server'
import QuoteCard from './QuoteCard.vue'
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('displays a quote fetched via MSW', async () => {
const wrapper = mount(QuoteCard)
await flushPromises()
expect(wrapper.text()).toContain('MSW quote')
})
flushPromises() may still be needed here for the same reason — Vue’s reactivity system needs a tick to apply DOM updates after the promise resolves.
When to use this approach: Integration-style tests where you want to verify that your component works with realistic HTTP behavior, or when multiple components share the same endpoint. Vitest’s own documentation recommends MSW for request-level mocking in Node-based test environments.
Choosing Between vi.mock and MSW
vi.mock | MSW | |
|---|---|---|
| Intercepts at | Module level | Network level |
| Tests real fetch logic | ❌ | ✅ |
| Per-test control | ✅ Easy | ✅ Via server.use() |
| Best for | Unit tests | Integration tests |
Conclusion
Both strategies are valid tools for mocking API calls in Vue tests. Use vi.mock when you want fast, isolated unit tests with precise control over return values. Use MSW when you want your tests to exercise the full request path. In either case, utilities like flushPromises() help ensure Vue finishes rendering async updates before assertions run.
FAQs
Yes. Many teams use vi.mock for focused unit tests of individual components and MSW for broader integration tests that span multiple components or services. Keep them in separate test files to avoid confusion about which layer is handling the mock.
Vue may still render the loading state when your assertion runs. If your assertion happens to match loading text, the test passes by coincidence. The console warning typically signals unhandled promise rejections or missing awaits. Calling flushPromises after mounting components that trigger async requests helps avoid these timing issues.
MSW intercepts at the network level, so it works with any HTTP client including axios, fetch, and libraries built on top of them like ky or got. Your choice of HTTP client does not affect how MSW handles the interception.
With vi.mock, call mockRejectedValue to simulate a thrown async error. With MSW, use server.use inside a specific test to register a handler that returns HttpResponse.json with a 4xx or 5xx status code. Both approaches let you verify your component's error handling.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.