Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions docs/api/vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,11 @@ If there is no `__mocks__` folder or a factory provided, Vitest will import the
function doMock(
path: string,
factory?: MockOptions | MockFactory<unknown>
): void
): Disposable
function doMock<T>(
module: Promise<T>,
factory?: MockOptions | MockFactory<T>
): void
): Disposable
```

The same as [`vi.mock`](#vi-mock), but it's not hoisted to the top of the file, so you can reference variables in the global file scope. The next [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) of the module will be mocked.
Expand Down Expand Up @@ -229,6 +229,24 @@ test('importing the next module imports mocked one', async () => {
})
```

::: tip
In environments that support [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management), you can use `using` on the value returned from `vi.doMock()` to automatically call [`vi.doUnmock()`](#vi-dounmock) on the mocked module when the containing block is exited. This is especially useful when mocking a dynamically imported module for a single test case.

```ts
it('uses a mocked version of my-module', () => {
using _mockDisposable = vi.doMock('my-module')

const myModule = await import('my-module') // mocked

// my-module is restored here
})

it('uses the normal version of my-module again', () => {
const myModule = await import('my-module') // not mocked
})
```
:::

### vi.mocked

```ts
Expand Down
14 changes: 12 additions & 2 deletions packages/vitest/src/integrations/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,13 @@ export interface VitestUtils {
* Mocking algorithm is described in [documentation](https://vitest.dev/guide/mocking/modules).
* @param path Path to the module. Can be aliased, if your Vitest config supports it
* @param factory Mocked module factory. The result of this function will be an exports object
*
* @returns A disposable object that calls {@link doUnmock()} when disposed
*/
// eslint-disable-next-line ts/method-signature-style
doMock(path: string, factory?: MockFactoryWithHelper | MockOptions): void
doMock(path: string, factory?: MockFactoryWithHelper | MockOptions): Disposable
// eslint-disable-next-line ts/method-signature-style
doMock<T>(module: Promise<T>, factory?: MockFactoryWithHelper<T> | MockOptions): void
doMock<T>(module: Promise<T>, factory?: MockFactoryWithHelper<T> | MockOptions): Disposable
/**
* Removes module from mocked registry. All subsequent calls to import will return original module.
*
Expand Down Expand Up @@ -617,6 +619,14 @@ function createVitest(): VitestUtils {
)
: factory,
)

const rv = {} as Disposable
if (Symbol.dispose) {
rv[Symbol.dispose] = () => {
_mocker().queueUnmock(path, importer)
}
}
return rv
},

doUnmock(path: string | Promise<unknown>) {
Expand Down
32 changes: 32 additions & 0 deletions test/core/test/do-mock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,35 @@ test('the second doMock can override the first doMock', async () => {

expect(incrementWith20(1)).toBe(21)
})

test.runIf(Symbol.dispose)('doMock works with using', async () => {
vi.doUnmock('./fixtures/increment')

{
const { increment: incrementWith1 } = await import('./fixtures/increment')
expect(incrementWith1(1)).toBe(2)
}

{
using _incrementMock = vi.doMock('./fixtures/increment', () => ({
increment: (num: number) => num + 10,
}))

const { increment: incrementWith10 } = await import('./fixtures/increment')

expect(incrementWith10(1)).toBe(11)
}

{
const { increment: incrementWith1 } = await import('./fixtures/increment')
expect(incrementWith1(1)).toBe(2)
}
})

test.skipIf(Symbol.dispose)('doMock works with using', async () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test seems weird, because Symbol.dispose in _incrementMock?.[Symbol.dispose] is always undefined and test always checks for _incrementMock[undefined]

Maybe it should instead check Object.keys(_incrementMock)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had lifted the pattern from here:

describe.skipIf(Symbol.dispose)('in environments not supporting it', () => {
it('does not have dispose property', () => {
expect(vi.fn()[Symbol.dispose]).toBeUndefined()
})
})

I wasn't sure if Symbol.dispose was patched in between evaluating the skipIf condition and the test body or something, for example, by something like

Symbol.dispose ??= Symbol('dispose')


If Symbol.dispose really is undefined, then maybe we just skip the test altogether? There are already tests that assert that doMock()'s functionality works, so it doesn't really matter what the return value is in a non-using-supporting environment 🤷 Or, like you say, we could examine the keys of the object, but it's just a little weird to try to do that when we can't name the key that we're interested in.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, if there is no reason to test something, then don't test it

const _incrementMock = vi.doMock('./fixtures/increment', () => ({
increment: (num: number) => num + 10,
}))

expect(_incrementMock?.[Symbol.dispose]).toBeUndefined()
})
Loading