---
title: "Mocking fetch, modules and timers in Vitest · vuejs.school"
description: "Fake a network call, swap a module, control the clock for a debounce, and clean up after each test so a mock from one test never leaks into the next."
canonical_url: "https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest"
meta:
  "og:description": "Fake a network call, swap a module, control the clock for a debounce, and clean up after each test so a mock from one test never leaks into the next."
  "og:image": "https://vuejs.school/_og/d/a_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdA%2Cdescription_~RmFrZSBhIG5ldHdvcmsgY2FsbCwgc3dhcCBhIG1vZHVsZSwgY29udHJvbCB0aGUgY2xvY2sgZm9yIGEgZGVib3VuY2UsIGFuZCBjbGVhbiB1cCBhZnRlciBlYWNoIHRlc3Qgc28gYSBtb2NrIGZyb20gb25lIHRlc3QgbmV2ZXIgbGVha3MgaW50byB0aGUgbmV4dC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb2NraW5nLWZldGNoLW1vZHVsZXMtYW5kLXRpbWVycy1pbi12aXRlc3Qi%2Cs_QguzSX2XXa4ker2m.png"
  "og:image:alt": "Mocking fetch, modules and timers in Vitest · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Mocking fetch, modules and timers in Vitest · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdA%2Cdescription_~RmFrZSBhIG5ldHdvcmsgY2FsbCwgc3dhcCBhIG1vZHVsZSwgY29udHJvbCB0aGUgY2xvY2sgZm9yIGEgZGVib3VuY2UsIGFuZCBjbGVhbiB1cCBhZnRlciBlYWNoIHRlc3Qgc28gYSBtb2NrIGZyb20gb25lIHRlc3QgbmV2ZXIgbGVha3MgaW50byB0aGUgbmV4dC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb2NraW5nLWZldGNoLW1vZHVsZXMtYW5kLXRpbWVycy1pbi12aXRlc3Qi%2Cs_QguzSX2XXa4ker2m.png"
  "twitter:image:alt": "Mocking fetch, modules and timers in Vitest · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## vi.fn and vi.spyOn: the two kinds of fake

`vi.fn()` creates a new function that records every call. You can give it an implementation, or tell it what to return with `mockReturnValue` and `mockResolvedValue`, and then assert on it with `toHaveBeenCalledWith`. Use it when you are handing a function to the code under test: a callback, a prop, a dependency.

`vi.spyOn(object, 'method')` wraps a method that already exists. By default the real method still runs and the spy only records calls; chain `.mockReturnValue()` to replace the behaviour. Use it when the code under test reaches for something you did not pass in, such as `localStorage.getItem` or `console.warn`.

```
import { expect, it, vi } from 'vitest'

it('records calls', () => {
  const onSave = vi.fn(() => true)
  const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})

  onSave('draft')
  console.warn('unused prop')

  expect(onSave).toHaveBeenCalledWith('draft')
  expect(warn).toHaveBeenCalledTimes(1)
})
```

Both return the same kind of object, a mock, with the same methods. The difference is only where the function came from, and that decides how it is cleaned up, which we come back to at the end.

## Stubbing fetch for a component that loads data

Here is a component that loads a user when it mounts.

*src/components/UserCard.vue*

```
<script setup lang="ts">
import { onMounted, ref } from 'vue'

const props = defineProps<{ id: number }>()

const name = ref('')
const error = ref('')

onMounted(async () => {
  const response = await fetch(`/api/users/${props.id}`)
  if (!response.ok) {
    error.value = 'Could not load user'
    return
  }
  const user = await response.json()
  name.value = user.name
})
</script>

<template>
  <p v-if="error">{{ error }}</p>
  <p v-else>{{ name || 'Loading' }}</p>
</template>
```

`fetch` is a global, so the cleanest way to replace it is `vi.stubGlobal('fetch', mock)`. Vitest remembers the original and `vi.unstubAllGlobals()` puts it back. The mock resolves to the smallest object the component needs: `ok` and a `json()` method.

*src/components/\_\_tests\_\_/UserCard.spec.ts*

```
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, expect, it, vi } from 'vitest'
import UserCard from '../UserCard.vue'

const fetchMock = vi.fn()

afterEach(() => {
  vi.unstubAllGlobals()
})

it('shows the user name', async () => {
  fetchMock.mockResolvedValue({ ok: true, json: async () => ({ name: 'Ada' }) } as Response)
  vi.stubGlobal('fetch', fetchMock)

  const wrapper = mount(UserCard, { props: { id: 1 } })
  await flushPromises()

  expect(fetchMock).toHaveBeenCalledWith('/api/users/1')
  expect(wrapper.text()).toBe('Ada')
})

it('shows an error when the request fails', async () => {
  fetchMock.mockResolvedValue({ ok: false } as Response)
  vi.stubGlobal('fetch', fetchMock)

  const wrapper = mount(UserCard, { props: { id: 2 } })
  await flushPromises()

  expect(wrapper.text()).toBe('Could not load user')
})
```

`flushPromises()` from Vue Test Utils is the line that makes this work. `mount` returns while `onMounted` is still waiting for `fetch`, so without it the first assertion would see `Loading`. `flushPromises` waits until every pending promise callback has run, including the two `await`s in the component and the DOM update that follows, and then your assertions see the finished state. Reach for it after any action that triggers async work; `await nextTick()` only waits for one render, not for a promise chain.

## Replacing a module with vi.mock

Stubbing `fetch` works, but as the app grows the component usually calls a function in an API module instead. Then the thing to fake is the module. Suppose the request moves to `src/api/users.ts`:

*src/api/users.ts*

```
export async function fetchUser(id: number): Promise<{ name: string }> {
  const response = await fetch(`/api/users/${id}`)
  if (!response.ok) throw new Error(`Request failed: ${response.status}`)
  return response.json()
}
```

`UserCard.vue` now calls `fetchUser` and catches its error. Only the script changes; the template stays as it was:

*src/components/UserCard.vue*

```
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { fetchUser } from '../api/users'

const props = defineProps<{ id: number }>()

const name = ref('')
const error = ref('')

onMounted(async () => {
  try {
    const user = await fetchUser(props.id)
    name.value = user.name
  } catch {
    error.value = 'Could not load user'
  }
})
</script>
```

`vi.mock(path, factory)` replaces that module for the whole test file: the test's own import and the import inside `UserCard.vue` both get the fake. Write the path as the test file would import it. The factory returns what the fake module exports. `vi.mocked()` is a typing helper that tells TypeScript the import is now a mock, so `mockResolvedValue` is allowed on it. The test file now mocks the module instead of stubbing `fetch`:

*src/components/\_\_tests\_\_/UserCard.spec.ts*

```
import { flushPromises, mount } from '@vue/test-utils'
import { expect, it, vi } from 'vitest'
import { fetchUser } from '../../api/users'
import UserCard from '../UserCard.vue'

vi.mock('../../api/users', () => ({
  fetchUser: vi.fn(),
}))

it('shows the user name', async () => {
  vi.mocked(fetchUser).mockResolvedValue({ name: 'Ada' })

  const wrapper = mount(UserCard, { props: { id: 1 } })
  await flushPromises()

  expect(fetchUser).toHaveBeenCalledWith(1)
  expect(wrapper.text()).toBe('Ada')
})
```

One rule about `vi.mock` catches people out: it is hoisted. Vitest moves the call to the top of the file, above the imports, so that the fake is in place before `UserCard.vue` imports the module. That means the factory cannot use a variable you declared above it: at run time that line has not executed yet, and the file fails with `[vitest] There was an error when mocking a module`, caused by `ReferenceError: Cannot access '…' before initialization`. If you need the mock in a variable, create it with `const mocks = vi.hoisted(() => ({ fetchUser: vi.fn() }))`, which is hoisted too, and return it from the factory: `vi.mock('../../api/users', () => mocks)`.

To keep most of a module and replace one export, use the `importOriginal` function the factory receives: `vi.mock('../../api/users', async (importOriginal) => ({ ...(await importOriginal()), fetchUser: vi.fn() }))`.

Prefer the module mock to the `fetch` stub once the request has a named function. The test then says what it depends on (`fetchUser` returned Ada) rather than how it is implemented (a URL and a JSON body), and a change from `fetch` to a client library does not touch the test.

## Fake timers for a debounce

A debounce waits until the user has stopped typing before it acts. A test should not wait too. Here is a small composable that calls `search` 300 milliseconds after the last change to `query`:

*src/composables/useDebouncedSearch.ts*

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

export function useDebouncedSearch(search: (query: string) => void, delay = 300) {
  const query = ref('')
  let timer: ReturnType<typeof setTimeout> | undefined

  watch(query, value => {
    clearTimeout(timer)
    timer = setTimeout(() => search(value), delay)
  })

  return { query }
}
```

`vi.useFakeTimers()` replaces `setTimeout` and friends with a clock you control. `vi.advanceTimersByTime(300)` moves that clock forward and runs every timer that falls due, synchronously, so the assertion on the next line sees the result.

*src/composables/\_\_tests\_\_/useDebouncedSearch.spec.ts*

```
import { nextTick } from 'vue'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { useDebouncedSearch } from '../useDebouncedSearch'

beforeEach(() => {
  vi.useFakeTimers()
})

afterEach(() => {
  vi.useRealTimers()
})

it('calls search once, after the delay, with the last value', async () => {
  const search = vi.fn()
  const { query } = useDebouncedSearch(search)

  query.value = 'vu'
  await nextTick()
  query.value = 'vue'
  await nextTick()

  expect(search).not.toHaveBeenCalled()
  vi.advanceTimersByTime(299)
  expect(search).not.toHaveBeenCalled()
  vi.advanceTimersByTime(1)
  expect(search).toHaveBeenCalledTimes(1)
  expect(search).toHaveBeenCalledWith('vue')
})
```

The `await nextTick()` lines are there because a watcher does not run the moment you assign; Vue queues it and runs it in a microtask. Microtasks are not timers, so fake timers do not touch them, and `nextTick` still works.

Since Vitest 3, `useFakeTimers()` fakes every timer API it finds, including `performance.now()` and `requestAnimationFrame`, except `process.nextTick` and `queueMicrotask`. If something in the code under test reads `performance.now()`, it now reads a frozen clock until you advance it.

## The test that passes alone and fails in the suite

Every fake above outlives the test that created it unless something removes it. Vitest isolates test files from each other, but not the tests inside a file, so whatever one test leaves behind is still there when the next one runs. The result is a test that passes when you run it alone, with `it.only` or with `npx vitest -t` and part of its name, and fails when the whole file runs.

The module mock above already sets this trap. Its factory runs once, the first time the module is imported, so every test in `UserCard.spec.ts` shares the same `fetchUser` mock. Add a second test that counts the calls:

*src/components/\_\_tests\_\_/UserCard.spec.ts**Breaks***

```
it('asks for the user once', async () => {
  vi.mocked(fetchUser).mockResolvedValue({ name: 'Grace' })

  mount(UserCard, { props: { id: 2 } })
  await flushPromises()

  expect(fetchUser).toHaveBeenCalledTimes(1)
})
```

On its own, this test passes. In the full file it fails: `toHaveBeenCalledTimes(1)` finds two calls, because the call made in 'shows the user name' is still in the mock's history. Nothing in the failing test is wrong, so the error points away from the cause.

Other fakes leak the same way: a `fetch` stub that is never removed, or a `mockResolvedValue` set in test three that test seven silently relies on. Fake timers leak too. A test that calls `vi.useFakeTimers()` and never switches back leaves a frozen clock behind, and a later test that waits for a `setTimeout` to fire fails with `Error: Test timed out in 5000ms`, although nothing in it mentions timers.

## Cleaning up mocks and stubs in the config

The fix is to put the cleanup where it cannot be forgotten: in the config, once.

*vitest.config.ts**Works***

```
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vitest/config'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    restoreMocks: true,
    unstubGlobals: true,
    unstubEnvs: true,
  },
})
```

`restoreMocks: true` calls `mockRestore()` on every mock before each test: call history is cleared, a `vi.fn(impl)` goes back to `impl`, and a `vi.spyOn` puts the original method back on its object. With it, `fetchUser` starts every test with no calls, so the test above passes. `unstubGlobals` undoes every `vi.stubGlobal` (so the `afterEach` in the `fetch` test can go), and `unstubEnvs` undoes `vi.stubEnv`. No config option switches fake timers off, so keep the `afterEach(() => vi.useRealTimers())` next to every `useFakeTimers()`.

The three related options differ in how far they go:

- `clearMocks` only forgets calls.
- `mockReset` forgets calls and resets each implementation to the one it was created with.
- `restoreMocks` does both and also puts spied-on methods back on their objects.

Use `restoreMocks` and set what each test needs inside that test; a test that depends on a value set in a different test is already broken, it just has not failed yet. To find the ones that are, run the suite in a random order once: `npx vitest --run --sequence.shuffle`.

**Check it stuck**3 questions from the Testing async code and mocks quiz.

[**Practice now**](https://vuejs.school/learn/testing/testing-async-code-and-mocks?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 6 min read**Vitest 5: what changed for Vue tests**](https://vuejs.school/community/blog/vitest-5-what-changed-for-vue-tests) [Release notes · 6 min read**Vitest 5 release candidate: what's coming for Vue tests**](https://vuejs.school/community/blog/vitest-5-rc-whats-coming)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-08-05","description":"Fake a network call, swap a module, control the clock for a debounce, and clean up after each test so a mock from one test never leaks into the next.","name":"Mocking fetch, modules and timers in Vitest","url":"https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest","breadcrumb":{"@id":"https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-08-05","description":"Fake a network call, swap a module, control the clock for a debounce, and clean up after each test so a mock from one test never leaks into the next.","headline":"Mocking fetch, modules and timers in Vitest","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdA%2Cdescription_~RmFrZSBhIG5ldHdvcmsgY2FsbCwgc3dhcCBhIG1vZHVsZSwgY29udHJvbCB0aGUgY2xvY2sgZm9yIGEgZGVib3VuY2UsIGFuZCBjbGVhbiB1cCBhZnRlciBlYWNoIHRlc3Qgc28gYSBtb2NrIGZyb20gb25lIHRlc3QgbmV2ZXIgbGVha3MgaW50byB0aGUgbmV4dC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb2NraW5nLWZldGNoLW1vZHVsZXMtYW5kLXRpbWVycy1pbi12aXRlc3Qi%2Cs_QguzSX2XXa4ker2m.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/mocking-fetch-modules-and-timers-in-vitest#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Mocking fetch, modules and timers in Vitest","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdA%2Cdescription_~RmFrZSBhIG5ldHdvcmsgY2FsbCwgc3dhcCBhIG1vZHVsZSwgY29udHJvbCB0aGUgY2xvY2sgZm9yIGEgZGVib3VuY2UsIGFuZCBjbGVhbiB1cCBhZnRlciBlYWNoIHRlc3Qgc28gYSBtb2NrIGZyb20gb25lIHRlc3QgbmV2ZXIgbGVha3MgaW50byB0aGUgbmV4dC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb2NraW5nLWZldGNoLW1vZHVsZXMtYW5kLXRpbWVycy1pbi12aXRlc3Qi%2Cs_QguzSX2XXa4ker2m.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdCDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~TW9ja2luZyBmZXRjaCwgbW9kdWxlcyBhbmQgdGltZXJzIGluIFZpdGVzdA%2Cdescription_~RmFrZSBhIG5ldHdvcmsgY2FsbCwgc3dhcCBhIG1vZHVsZSwgY29udHJvbCB0aGUgY2xvY2sgZm9yIGEgZGVib3VuY2UsIGFuZCBjbGVhbiB1cCBhZnRlciBlYWNoIHRlc3Qgc28gYSBtb2NrIGZyb20gb25lIHRlc3QgbmV2ZXIgbGVha3MgaW50byB0aGUgbmV4dC4%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9tb2NraW5nLWZldGNoLW1vZHVsZXMtYW5kLXRpbWVycy1pbi12aXRlc3Qi%2Cs_QguzSX2XXa4ker2m.png"}]}
```