diff --git a/docs/api/index.md b/docs/api/index.md index 706bfd8d0fdf..e6c896cfbd50 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -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` diff --git a/docs/config/index.md b/docs/config/index.md index a8d089e50792..c94cedaeeb31 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -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? }` diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 1a55177c96d0..e483e55402cb 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -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' @@ -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, + } +} +``` diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 12cb89f21097..8dcd68276739 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -220,21 +220,58 @@ class VitestUtils { return this } + private _stubsGlobal = new Map() + 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 } diff --git a/packages/vitest/src/runtime/run.ts b/packages/vitest/src/runtime/run.ts index 917d2453dd20..abe8d47c6d98 100644 --- a/packages/vitest/src/runtime/run.ts +++ b/packages/vitest/src/runtime/run.ts @@ -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) @@ -504,4 +504,9 @@ export function clearModuleMocks() { vi.resetAllMocks() else if (clearMocks) vi.clearAllMocks() + + if (unstubEnvs) + vi.unstubAllEnvs() + if (unstubGlobals) + vi.unstubAllGlobals() } diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 81453e184fef..1b50c805262a 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -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. * diff --git a/test/core/test/stubs.test.ts b/test/core/test/stubs.test.ts new file mode 100644 index 000000000000..2f45cb52a859 --- /dev/null +++ b/test/core/test/stubs.test.ts @@ -0,0 +1,87 @@ +/* eslint-disable vars-on-top */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +declare global { + // eslint-disable-next-line no-var + var __defined__: unknown +} + +describe('stubbing globals', () => { + beforeEach(() => { + delete globalThis.__defined__ + vi.unstubAllGlobals() + }) + + it('stubs and restores already defined value', () => { + globalThis.__defined__ = 'true' + vi.stubGlobal('__defined__', 'false') + expect(__defined__).toBe('false') + expect(globalThis.__defined__).toBe('false') + vi.unstubAllGlobals() + expect(__defined__).toBe('true') + expect(globalThis.__defined__).toBe('true') + }) + + it('stubs and removes undefined value', () => { + vi.stubGlobal('__defined__', 'false') + expect(__defined__).toBe('false') + expect(globalThis.__defined__).toBe('false') + vi.unstubAllGlobals() + expect('__defined__' in globalThis).toBe(false) + expect(() => __defined__).toThrowError(ReferenceError) + expect(globalThis.__defined__).toBeUndefined() + }) + + it('restores the first available value', () => { + globalThis.__defined__ = 'true' + vi.stubGlobal('__defined__', 'false') + vi.stubGlobal('__defined__', false) + vi.stubGlobal('__defined__', null) + expect(__defined__).toBe(null) + expect(globalThis.__defined__).toBe(null) + vi.unstubAllGlobals() + expect(__defined__).toBe('true') + expect(globalThis.__defined__).toBe('true') + }) +}) + +describe('stubbing envs', () => { + beforeEach(() => { + process.env.VITE_TEST_UPDATE_ENV = 'development' + vi.unstubAllEnvs() + }) + + it('stubs and restores env', () => { + vi.stubEnv('VITE_TEST_UPDATE_ENV', 'production') + expect(import.meta.env.VITE_TEST_UPDATE_ENV).toBe('production') + expect(process.env.VITE_TEST_UPDATE_ENV).toBe('production') + vi.unstubAllEnvs() + expect(import.meta.env.VITE_TEST_UPDATE_ENV).toBe('development') + expect(process.env.VITE_TEST_UPDATE_ENV).toBe('development') + }) + + it('stubs and restores previously not defined env', () => { + delete process.env.VITE_TEST_UPDATE_ENV + vi.stubEnv('VITE_TEST_UPDATE_ENV', 'production') + expect(import.meta.env.VITE_TEST_UPDATE_ENV).toBe('production') + expect(process.env.VITE_TEST_UPDATE_ENV).toBe('production') + vi.unstubAllEnvs() + expect('VITE_TEST_UPDATE_ENV' in process.env).toBe(false) + expect('VITE_TEST_UPDATE_ENV' in import.meta.env).toBe(false) + expect(import.meta.env.VITE_TEST_UPDATE_ENV).toBeUndefined() + expect(process.env.VITE_TEST_UPDATE_ENV).toBeUndefined() + }) + + it('restores the first available value', () => { + globalThis.__defined__ = 'true' + vi.stubEnv('VITE_TEST_UPDATE_ENV', 'production') + vi.stubEnv('VITE_TEST_UPDATE_ENV', 'staging') + vi.stubEnv('VITE_TEST_UPDATE_ENV', 'test') + expect(import.meta.env.VITE_TEST_UPDATE_ENV).toBe('test') + expect(process.env.VITE_TEST_UPDATE_ENV).toBe('test') + vi.unstubAllEnvs() + expect(import.meta.env.VITE_TEST_UPDATE_ENV).toBe('development') + expect(process.env.VITE_TEST_UPDATE_ENV).toBe('development') + }) +})