#Ho do I properly mock UseFetch in vitest

1 messages · Page 1 of 1 (latest)

ancient solarBOT
#

Hi @dim knot. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

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:

  1. useFetch returns an augmented promise (a promise with extra properties like data, status, refresh, etc.). [wrap useFetch]
  2. When you don’t await it, 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):

  1. Your composable might be await‑ing useFetch.
    If useMasterclass does something like:

    const { data, status } = await useFetch('/api/...')
    

    then useFetch must return a thenable (a promise) that resolves to { data, status, ... }. Right now you’re returning a plain object, so await useFetch() will resolve to that object, but if your composable expects the augmented promise behavior (e.g. using it without await somewhere), the shape may not match.

#
  1. Your mock may not be used at the right time.
    vi.mock('nuxt/app', ...) is hoisted; but mockUseFetch calls vi.mocked(nuxtApp.useFetch) at runtime. If useMasterclass imports useFetch directly from 'nuxt/app', it will get the mocked function, but if it’s using Nuxt auto‑imports (useFetch from #imports), your mock won’t affect it at all. The sources show that for auto‑imports, the recommended way is to use mockNuxtImport from nuxt-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 useMasterclass does const { data } = await useFetch(...), it gets result.
  • If it does const { data } = useFetch(...) without await, it still sees data and status on 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.

dim knot
#

@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

ancient solarBOT
#

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 mockNuxtImport for Nuxt auto‑imports like useSupabaseClient / 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 of null

means the useFetch mock (or its returned refs) is shared across tests and not reset.

Key points

  1. vi.clearAllMocks() only clears call history, not the implementation or the data inside refs you created earlier.
  2. If you create refs once and reuse them, their .value will 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()
})
#

If you still see { name: 'Test' } in the “null” test, it means some test is overriding the implementation and you’re not resetting it (or not recreating refs) before the next test. Moving all mockImplementation calls into beforeEach (and using fresh refs there) is what prevents that leak.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: