Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce a native way to set env and globals #2515

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
118 changes: 117 additions & 1 deletion docs/api/index.md
Expand Up @@ -2692,10 +2692,126 @@ Vitest provides utility functions to help you out through it's **vi** helper. Yo

### vi.restoreCurrentDate

- **Type**: `() => void`
- **Type:** `() => void`

Restores `Date` back to its native implementation.

### vi.stubEnv

- **Type:** `(name: string, value: string) => Vitest`

Changes the value of environmental variable on `process.env` and `import.meta.env`. You can restore its value by calling `vi.unstubAllEnvs`.

```ts
import { vi } from 'vitest'

// `process.env.NODE_ENV` and `import.meta.env.NODE_ENV`
// are "development" before calling "vi.stubEnv"

vi.stubEnv('NODE_ENV', 'production')

process.env.NODE_ENV === 'production'
import.meta.env.NODE_ENV === 'production'
// doesn't change other envs
import.meta.env.MODE === 'development'
```

:::tip
You can also change the value by simply assigning it, but you won't be able to use `vi.unstubAllEnvs` to restore previous value:

```ts
import.meta.env.MODE = 'test'
```
:::

:::warning
Vitest transforms all `import.meta.env` calls into `process.env`, so they can be easily changed at runtime. Node.js only supports string values as env parameters, while Vite supports several built-in envs as boolean (namely, `SSR`, `DEV`, `PROD`). To mimic Vite, set "truthy" values as env: `''` instead of `false`, and `'1'` instead of `true`.

But beware that you cannot rely on `import.meta.env.DEV === false` in this case. Use `!import.meta.env.DEV`. This also affects simple assigning, not just `vi.stubEnv` method.
:::

### vi.unstubAllEnvs

- **Type:** `() => Vitest`

Restores all `import.meta.env` and `process.env` values that were changed with `vi.stubEnv`. When it's called for the first time, Vitest remembers the original value and will store it, until `unstubAllEnvs` is called again.

```ts
import { vi } from 'vitest'

// `process.env.NODE_ENV` and `import.meta.env.NODE_ENV`
// are "development" before calling stubEnv

vi.stubEnv('NODE_ENV', 'production')

process.env.NODE_ENV === 'production'
import.meta.env.NODE_ENV === 'production'

vi.stubEnv('NODE_ENV', 'staging')

process.env.NODE_ENV === 'staging'
import.meta.env.NODE_ENV === 'staging'

vi.unstubAllEnvs()

// restores to the value that were stored before the first "stubEnv" call
process.env.NODE_ENV === 'development'
import.meta.env.NODE_ENV === 'development'
```

### vi.stubGlobal

- **Type:** `(name: stirng | number | symbol, value: uknown) => Vitest`

Changes the value of global variable. You can restore its original value by calling `vi.unstubAllGlobals`.

```ts
import { vi } from 'vitest'

// `innerWidth` is "0" before callling stubGlobal

vi.stubGlobal('innerWidth', 100)

innerWidth === 100
window.innerWidth === 100
```

:::tip
You can also change the value by simply assigning it to `globalThis` or `window`, but you won't be able to use `vi.unstubAllGlobals` to restore previous value:

```ts
window.innerWidth = 100
```
:::

### vi.unstubAllGlobals

- **Type:** `() => Vitest`

Restores all global values on `globalThis`/`global`/`window` that were changed with `vi.stubGlobal`. When it's called for the first time, Vitest remembers the original value and will store it, until `unstubAllGlobals` is called again.

```ts
import { vi } from 'vitest'

const Mock = vi.fn()

// IntersectionObserver is "undefined" before calling "stubGlobal"

vi.stubGlobal('IntersectionObserver', Mock)

IntersectionObserver === Mock
global.IntersectionObserver === Mock
globalThis.IntersectionObserver === Mock
window.IntersectionObserver === Mock

vi.unstubAllGlobals()

window.IntersectionObserver === undefined
'IntersectionObserver' in window === false
// throws ReferenceError, because it's not defined
IntersectionObserver === undefined
```

### vi.runAllTicks

- **Type:** `() => Vitest`
Expand Down
14 changes: 14 additions & 0 deletions docs/config/index.md
Expand Up @@ -777,6 +777,20 @@ Will call [`.mockReset()`](/api/#mockreset) on all spies before each test. This

Will call [`.mockRestore()`](/api/#mockrestore) on all spies before each test. This will clear mock history and reset its implementation to the original one.

### unstubEnvs

- **Type:** `boolean`
- **Default:** `false`

Will call [`vi.unstubAllEnvs`](/api/#vi-unstuballenvs) before each test.

### unstubGlobals

- **Type:** `boolean`
- **Default:** `false`

Will call [`vi.unstubAllGlobals`](/api/#vi-unstuballglobals) before each test.

### transformMode

- **Type:** `{ web?, ssr? }`
Expand Down
65 changes: 62 additions & 3 deletions docs/guide/mocking.md
Expand Up @@ -399,16 +399,20 @@ vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked')

Example with `vi.mock`:
```ts
// some-path.ts
// ./some-path.ts
export function method() {}
```
```ts
import { method } from 'some-path'
vi.mock('some-path', () => ({
import { method } from './some-path.ts'
vi.mock('./some-path.ts', () => ({
method: vi.fn()
}))
```

::: warning
Don't forget that `vi.mock` call is hoisted to top of the file. **Do not** put `vi.mock` calls inside `beforeEach`, only one of these will actually mock a module.
:::

Example with `vi.spyOn`:
```ts
import * as exports from 'some-path'
Expand Down Expand Up @@ -511,16 +515,71 @@ mocked() // is a spy function

- Mock current date

To mock `Date`'s time, you can use `vi.setSystemTime` helper function. This value will **not** automatically reset between different tests.

Beware that using `vi.useFakeTimers` also changes the `Date`'s time.

```ts
const mockDate = new Date(2022, 0, 1)
vi.setSystemTime(mockDate)
const now = new Date()
expect(now.valueOf()).toBe(mockDate.valueOf())
// reset mocked time
vi.useRealTimers()
```

- Mock global variable

You can set global variable by assigning a value to `globalThis` or using [`vi.stubGlobal`](/api/#vi-stubglobal) helper. When using `vi.stubGlobal`, it will **not** automatically reset between different tests, unless you enable [`unstubGlobals`](/config/#unstubglobals) config option or call [`vi.unstubAllGlobals`](/api/#vi-unstuballglobals).

```ts
vi.stubGlobal('__VERSION__', '1.0.0')
expect(__VERSION__).toBe('1.0.0')
```

- Mock `import.meta.env`

To change environmental variable, you can just assign a new value to it. This value will **not** automatically reset between different tests.

```ts
import { beforeEach, expect, it } from 'vitest'

// you can reset it in beforeEach hook manually
const originalViteEnv = import.meta.env.VITE_ENV

beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv
})

it('changes value', () => {
import.meta.env.VITE_ENV = 'staging'
expect(import.meta.env.VITE_ENV).toBe('staging')
})
```

If you want to automatically reset value, you can use `vi.stubEnv` helper with [`unstubEnvs`](/config/#unstubEnvs) config option enabled (or call [`vi.unstubAllEnvs`](/api/#vi-unstuballenvs) manually in `beforeEach` hook):

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

// before running tests "VITE_ENV" is "test"
import.meta.env.VITE_ENV === 'test'

it('changes value', () => {
vi.stubEnv('VITE_ENV', 'staging')
expect(import.meta.env.VITE_ENV).toBe('staging')
})

it('the value is restored before running an other test', () => {
expect(import.meta.env.VITE_ENV).toBe('test')
})
```

```ts
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
}
}
```
59 changes: 48 additions & 11 deletions packages/vitest/src/integrations/vi.ts
Expand Up @@ -220,21 +220,58 @@ class VitestUtils {
return this
}

private _stubsGlobal = new Map<string | symbol | number, PropertyDescriptor | undefined>()
private _stubsEnv = new Map()

/**
* Will put a value on global scope. Useful, if you are
* using jsdom/happy-dom and want to mock global variables, like
* `IntersectionObserver`.
* Makes value available on global namespace.
* Useful, if you want to have global variables available, like `IntersectionObserver`.
* You can return it back to original value with `vi.unstubGlobals`, or by enabling `unstubGlobals` config option.
*/
public stubGlobal(name: string | symbol | number, value: any) {
if (globalThis.window) {
// @ts-expect-error we can do anything!
globalThis.window[name] = value
}
else {
// @ts-expect-error we can do anything!
globalThis[name] = value
}
if (!this._stubsGlobal.has(name))
this._stubsGlobal.set(name, Object.getOwnPropertyDescriptor(globalThis, name))
// @ts-expect-error we can do anything!
globalThis[name] = value
return this
}

/**
* Changes the value of `import.meta.env` and `process.env`.
* You can return it back to original value with `vi.unstubEnvs`, or by enabling `unstubEnvs` config option.
*/
public stubEnv(name: string, value: string) {
if (!this._stubsEnv.has(name))
this._stubsEnv.set(name, process.env[name])
process.env[name] = value
return this
}

/**
* Reset the value to original value that was available before first `vi.stubGlobal` was called.
*/
public unstubAllGlobals() {
this._stubsGlobal.forEach((original, name) => {
if (!original)
Reflect.deleteProperty(globalThis, name)
else
Object.defineProperty(globalThis, name, original)
})
this._stubsGlobal.clear()
return this
}

/**
* Reset enviromental variables to the ones that were available before first `vi.stubEnv` was called.
*/
public unstubAllEnvs() {
this._stubsEnv.forEach((original, name) => {
if (original === undefined)
delete process.env[name]
else
process.env[name] = original
})
this._stubsEnv.clear()
return this
}

Expand Down
7 changes: 6 additions & 1 deletion packages/vitest/src/runtime/run.ts
Expand Up @@ -495,7 +495,7 @@ export async function startTests(paths: string[], config: ResolvedConfig) {
}

export function clearModuleMocks() {
const { clearMocks, mockReset, restoreMocks } = getWorkerState().config
const { clearMocks, mockReset, restoreMocks, unstubEnvs, unstubGlobals } = getWorkerState().config

// since each function calls another, we can just call one
if (restoreMocks)
Expand All @@ -504,4 +504,9 @@ export function clearModuleMocks() {
vi.resetAllMocks()
else if (clearMocks)
vi.clearAllMocks()

if (unstubEnvs)
vi.unstubAllEnvs()
if (unstubGlobals)
vi.unstubAllGlobals()
}
12 changes: 12 additions & 0 deletions packages/vitest/src/types/config.ts
Expand Up @@ -284,6 +284,18 @@ export interface InlineConfig {
*/
restoreMocks?: boolean

/**
* Will restore all global stubs to their original values before each test
* @default false
*/
unstubGlobals?: boolean

/**
* Will restore all env stubs to their original values before each test
* @default false
*/
unstubEnvs?: boolean

/**
* Serve API options.
*
Expand Down