#Ho do I properly mock UseFetch in vitest
1 messages · Page 1 of 1 (latest)
The knowledge sources don’t contain an example of mocking useFetch in Vitest directly, but they do explain two key things that affect your test:
useFetchreturns an augmented promise (a promise with extra properties likedata,status,refresh, etc.). [wrap useFetch]- When you don’t
awaitit, you normally rely on those augmented properties being present immediately (even though the promise resolves later). [wrap useFetch]
Because of that, your mock needs to look like what useFetch actually returns in your composable.
What’s likely going wrong
In your test:
vi.mock('nuxt/app', () => ({
useRoute: () => ({
params: { id: '123' },
}),
useFetch: vi.fn(),
}))
and later:
function mockUseFetch(data: any = null, status: any = 'success') {
const mock = {
data: ref(data ?? null),
status: ref(status),
execute: vi.fn(),
refresh: vi.fn(),
}
vi.mocked(nuxtApp.useFetch).mockReturnValue(mock)
return mock
}
Two common pitfalls here (based on how Nuxt composables work):
-
Your composable might be
await‑inguseFetch.
IfuseMasterclassdoes something like:const { data, status } = await useFetch('/api/...')then
useFetchmust return a thenable (a promise) that resolves to{ data, status, ... }. Right now you’re returning a plain object, soawait useFetch()will resolve to that object, but if your composable expects the augmented promise behavior (e.g. using it withoutawaitsomewhere), the shape may not match.
- Your mock may not be used at the right time.
vi.mock('nuxt/app', ...)is hoisted; butmockUseFetchcallsvi.mocked(nuxtApp.useFetch)at runtime. IfuseMasterclassimportsuseFetchdirectly from'nuxt/app', it will get the mocked function, but if it’s using Nuxt auto‑imports (useFetchfrom#imports), your mock won’t affect it at all. The sources show that for auto‑imports, the recommended way is to usemockNuxtImportfromnuxt-vitest/utils. [mock Supabase; mock useHead]
A safer pattern (matching augmented promise)
From the “wrap useFetch” answer, we know the correct return type is essentially:
ReturnType<typeof useFetch<T>>
and that it’s an augmented promise. [wrap useFetch]
So your mock can be shaped like this:
import { mockNuxtImport } from 'nuxt-vitest/utils'
import { ref } from 'vue'
function createUseFetchMock(data: any = null, status: any = 'success') {
const result = {
data: ref(data),
status: ref(status),
execute: vi.fn(),
refresh: vi.fn(),
}
// Make it a thenable (augmented promise)
const promise: any = Promise.resolve(result)
Object.assign(promise, result)
return promise
}
// If your composable uses auto-imported useFetch:
mockNuxtImport('useFetch', () => {
return vi.fn(() => createUseFetchMock(null, 'success'))
})
This way:
- If
useMasterclassdoesconst { data } = await useFetch(...), it getsresult. - If it does
const { data } = useFetch(...)withoutawait, it still seesdataandstatuson the returned object (because they’re assigned onto the promise). [wrap useFetch]
Then your test becomes:
describe('initial state', () => {
it('initializes with null masterclass', () => {
// Override default mock for this test if needed
mockNuxtImport('useFetch', () => {
return vi.fn(() => createUseFetchMock(null, 'success'))
})
const { masterclass, hasMasterclass } = useMasterclass()
expect(masterclass.value).toBeNull()
expect(hasMasterclass.value).toBe(false)
})
})
If you must keep vi.mock('nuxt/app', ...)
If useMasterclass imports useFetch from 'nuxt/app' (not auto‑import), you can adapt the same idea:
vi.mock('nuxt/app', () => {
const useFetch = vi.fn((..._args) => {
const result = {
data: ref(null),
status: ref('success'),
execute: vi.fn(),
refresh: vi.fn(),
}
const promise: any = Promise.resolve(result)
Object.assign(promise, result)
return promise
})
return {
useRoute: () => ({ params: { id: '123' } }),
useFetch,
}
})
This aligns your mock with how useFetch actually behaves per the Nuxt composable design, which should fix the expected undefined to be null assertion (that’s usually masterclass.value being undefined because the mocked useFetch didn’t populate it as your composable expects).
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@dim knot Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@ancient solar how do I get the mock to rest for every test? I'm getting this error:
Test: useMasterclass.nuxt.test.ts > useMasterclass > initial state > initializes with null masterclass
File: file:///Users/jamesphillips/Projects/self-serve/app/composables/useMasterclass.nuxt.test.ts
AssertionError: expected { name: 'Test' } to be null
at /Users/jamesphillips/Projects/self-serve/app/composables/useMasterclass.nuxt.test.ts:91:7
Let me check the knowledge base for you again. One moment! :books:
The knowledge sources don’t show an example of resetting a useFetch mock between tests, but they do show two relevant patterns:
- Using
mockNuxtImportfor Nuxt auto‑imports likeuseSupabaseClient/useHead. [mock Supabase; mock useHead] - Relying on Vitest’s normal mock lifecycle (
beforeEach,afterEach,vi.clearAllMocks). [mock Supabase]
Your symptom:
first test passes, later test still sees
{ name: 'Test' }instead ofnull
means the useFetch mock (or its returned refs) is shared across tests and not reset.
Key points
vi.clearAllMocks()only clears call history, not the implementation or the data inside refs you created earlier.- If you create refs once and reuse them, their
.valuewill persist across tests.
A pattern that resets per test
Using mockNuxtImport (for auto‑imported useFetch):
import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest'
import { ref } from 'vue'
import { mockNuxtImport, clearNuxtImport } from 'nuxt-vitest/utils'
import { useMasterclass } from './useMasterclass'
describe('useMasterclass', () => {
beforeEach(() => {
// fresh mock implementation for each test
mockNuxtImport('useFetch', () => {
return vi.fn(() => {
const result = {
data: ref(null), // <- starts as null every test
status: ref('success'),
execute: vi.fn(),
refresh: vi.fn(),
}
const promise: any = Promise.resolve(result)
Object.assign(promise, result)
return promise
})
})
})
afterEach(() => {
vi.clearAllMocks()
clearNuxtImport('useFetch') // remove mock so next test can re‑register
})
it('initializes with null masterclass', () => {
const { masterclass, hasMasterclass } = useMasterclass()
expect(masterclass.value).toBeNull()
expect(hasMasterclass.value).toBe(false)
})
it('can initialize with a masterclass', () => {
// override just for this test
mockNuxtImport('useFetch', () => {
return vi.fn(() => {
const result = {
data: ref({ name: 'Test' }),
status: ref('success'),
execute: vi.fn(),
refresh: vi.fn(),
}
const promise: any = Promise.resolve(result)
Object.assign(promise, result)
return promise
})
})
const { masterclass, hasMasterclass } = useMasterclass()
expect(masterclass.value).toEqual({ name: 'Test' })
expect(hasMasterclass.value).toBe(true)
})
})
This ensures:
- Each test gets a **fresh `useFetch` implementation**.
- Each call to `useFetch` returns **new refs** (`ref(null)` or `ref({ name: 'Test' })`), so state from one test can’t leak into another.
### If you’re mocking `'nuxt/app'` directly
You can apply the same idea: define the implementation in `beforeEach` so it’s recreated per test, and don’t reuse refs across tests:
```ts
vi.mock('nuxt/app', () => {
return {
useRoute: () => ({ params: { id: '123' } }),
useFetch: vi.fn(), // implementation set in beforeEach
}
})
import * as nuxtApp from 'nuxt/app'
beforeEach(() => {
vi.mocked(nuxtApp.useFetch).mockImplementation(() => {
const result = {
data: ref(null),
status: ref('success'),
execute: vi.fn(),
refresh: vi.fn(),
}
const promise: any = Promise.resolve(result)
Object.assign(promise, result)
return promise
})
})
afterEach(() => {
vi.clearAllMocks()
})