nuxt / test-utils

🧪 Test utilities for Nuxt
http://nuxt.com/docs/getting-started/testing
MIT License
323 stars 84 forks source link

Vitest restore mock sets all hoisted mocks to undefined when following documentation #864

Closed rstainsby closed 1 month ago

rstainsby commented 5 months ago

Environment

Reproduction

https://stackblitz.com/edit/github-htfm4n?file=README.md

Describe the bug

Nuxt's testing documentation recommends restoring mocks that you have hoisted using vi.hoisted() https://nuxt.com/docs/getting-started/testing#built-in-mocks.

The approach the documentation recommends for this is:

const { useStorageMock } = vi.hoisted(() => {
    return { useStorageMock: vi.fn().mockImplementation(() => {
      return { value: 'mocked storage'}
    })
  }
})

mockNuxtImport('useStorage', () => {
  return useStorageMock
})

// Then, inside a test
useStorageMock.mockImplementation(() => {
  return { value: 'something else' }
})

but when using vitests mock restore, this results in useStorageMock being immediately restored to undefined. So, unless you mock useStorageMock in every test, it will likely cause broken tests. This doesn't feel like the implied behavior - In the example above I expected restoring the mock to reset 'value' to 'mocked storage'.

This happens because mockRestore will restore a mocked function (vi.fn()) to undefined if that mocked function doesn't have an initial value (vi.fn(initalval)). Docs on that are here: https://vitest.dev/api/mock.html#mockrestore.

The solution for this then seems to be to define a initial value for the mocked function. So I propose that the docs are changed to:

const { useStorageMock } = vi.hoisted(() => {
    return { 
        useStorageMock: vi.fn(() => {
          return { value: 'mocked storage' }
        })
     }
})

mockNuxtImport('useStorage', () => {
  return useStorageMock
})

// Then, inside a test
useStorageMock.mockImplementation(() => {
  return { value: 'something else' }
})

I've included a stackblitz with tests that showcase this. More detail is included in the README of the stackblitz.

Additional context

TLDR: If you follow the docs recommendation of calling mockRestore before each test, you will end up with undefined mocks for every mocked Nuxt import that requires different implementations between tests.

Logs

No response

dosubot[bot] commented 5 months ago

Hey @rstainsby! 👋 I'm here to help you with any bugs, questions, or contributions you might have. I'm a bot that can assist with all things related to software development.

To address the issue where mockRestore restores a mocked function to undefined, you can define an initial value for the mocked function. Here is an updated version of the documentation example:

const { useStorageMock } = vi.hoisted(() => {
  return { 
    useStorageMock: vi.fn(() => {
      return { value: 'mocked storage' }
    })
  }
})

mockNuxtImport('useStorage', () => {
  return useStorageMock
})

// Then, inside a test
useStorageMock.mockImplementation(() => {
  return { value: 'something else' }
})

This approach ensures that the mocked function has a defined initial value, avoiding the issue where mockRestore restores it to undefined. This solution aligns with the behavior described in the Vitest documentation [1].

To continue talking to Dosu, mention @dosu.

rstainsby commented 5 months ago

Feel free to dismiss this if you feel this is expected behavior. It's just that, for me, the docs imply that the initial hoisted mock value would be the restore state.

dosubot[bot] commented 5 months ago

To clarify the behavior and avoid confusion, the Nuxt testing documentation can be updated to explicitly state that the initial hoisted mock value is not the restore state unless explicitly defined. Here is a suggested update to the documentation:

## Mocking in Nuxt Tests

When using the `mockNuxtImport` and `mockComponent` helpers, it's important to understand how the hoisted mock values are managed. By default, the initial hoisted mock value is not considered the restore state. This means that calling `mockRestore` will set all hoisted mocks to `undefined` unless you explicitly define the restore state.

### Example

```js
import { vi } from 'vitest';
import { mockNuxtImport, mockComponent } from 'nuxt-vitest-mocks';

vi.hoisted(() => {
  if (!globalThis.__NUXT_VITEST_MOCKS) {
    vi.stubGlobal('__NUXT_VITEST_MOCKS', {});
  }
});

// Define your mocks
mockNuxtImport('someModule', () => ({ someFunction: vi.fn(() => 'initial value') }));
mockComponent('SomeComponent', () => ({ default: vi.fn(() => 'initial value') }));

// Restore mocks
vi.mockRestore(); // This will set all hoisted mocks to `undefined` unless explicitly defined

To avoid issues with mockRestore, ensure that you define the restore state explicitly if needed.



This update clarifies the behavior and helps users avoid potential issues with undefined mocks when using `mockRestore` <sup>[[1]](https://github.com/nuxt/test-utils/blob/main/src/module/plugins/mock.ts)</sup>.

<!-- Dosu Comment Footer -->
*To continue talking to [Dosu](https://dosu.dev), mention @dosu.*