Skip to content

Commit

Permalink
feat: introduce a native way to set env and globals (#2515)
Browse files Browse the repository at this point in the history
* feat: introduce a native way to set env and globals

* chore: cleanup

* chore: cleanup

* chore: cleanup

* docs: cleanup
  • Loading branch information
sheremet-va committed Dec 16, 2022
1 parent eeea496 commit b114d49
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 16 deletions.
123 changes: 122 additions & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2692,10 +2692,131 @@ 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
globalThis.innerWidth === 100
// if you are using jsdom or happy-dom
window.innerWidth === 100
```

:::tip
You can also change the value by simply assigning it to `globalThis` or `window` (if you are using `jsdom` or `happy-dom` environment), but you won't be able to use `vi.unstubAllGlobals` to restore original value:

```ts
globalThis.innerWidth = 100
// if you are using jsdom or happy-dom
window.innerWidth = 100
```
:::

### vi.unstubAllGlobals

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

Restores all global values on `globalThis`/`global` (and `window`/`top`/`self`/`parent`, if you are using `jsdom` or `happy-dom` environment) 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
// if you are using jsdom or happy-dom
window.IntersectionObserver === Mock

vi.unstubAllGlobals()

globalThis.IntersectionObserver === undefined
'IntersectionObserver' in globalThis === 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit b114d49

Please sign in to comment.