diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 9e6b53a96..c693870c3 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -508,35 +508,35 @@ function guide(): DefaultTheme.SidebarItem[] { items: [ { text: 'Mocking Dates', - link: '/guide/mocking#dates', + link: '/guide/mocking/dates', }, { text: 'Mocking Functions', - link: '/guide/mocking#functions', + link: '/guide/mocking/functions', }, { text: 'Mocking Globals', - link: '/guide/mocking#globals', + link: '/guide/mocking/globals', }, { text: 'Mocking Modules', - link: '/guide/mocking-modules', + link: '/guide/mocking/modules', }, { - text: 'Mocking File System', - link: '/guide/mocking#file-system', + text: 'Mocking the File System', + link: '/guide/mocking/file-system', }, { text: 'Mocking Requests', - link: '/guide/mocking#requests', + link: '/guide/mocking/requests', }, { text: 'Mocking Timers', - link: '/guide/mocking#timers', + link: '/guide/mocking/timers', }, { text: 'Mocking Classes', - link: '/guide/mocking#classes', + link: '/guide/mocking/classes', }, ], }, diff --git a/docs/api/vi.md b/docs/api/vi.md index 2aee8dffe..2ef0c70ce 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -12,7 +12,7 @@ import { vi } from 'vitest' ## Mock Modules -This section describes the API that you can use when [mocking a module](/guide/mocking#modules). Beware that Vitest doesn't support mocking modules imported using `require()`. +This section describes the API that you can use when [mocking a module](/guide/mocking/modules). Beware that Vitest doesn't support mocking modules imported using `require()`. ### vi.mock @@ -171,7 +171,7 @@ axios.get(`/apples/${increment(1)}`) Beware that if you don't call `vi.mock`, modules **are not** mocked automatically. To replicate Jest's automocking behaviour, you can call `vi.mock` for each required module inside [`setupFiles`](/config/#setupfiles). ::: -If there is no `__mocks__` folder or a factory provided, Vitest will import the original module and auto-mock all its exports. For the rules applied, see [algorithm](/guide/mocking#automocking-algorithm). +If there is no `__mocks__` folder or a factory provided, Vitest will import the original module and auto-mock all its exports. For the rules applied, see [algorithm](/guide/mocking/modules#automocking-algorithm). ### vi.doMock @@ -295,7 +295,7 @@ vi.mock('./example.js', async () => { function importMock(path: string): Promise> ``` -Imports a module with all of its properties (including nested properties) mocked. Follows the same rules that [`vi.mock`](#vi-mock) does. For the rules applied, see [algorithm](/guide/mocking#automocking-algorithm). +Imports a module with all of its properties (including nested properties) mocked. Follows the same rules that [`vi.mock`](#vi-mock) does. For the rules applied, see [algorithm](/guide/mocking/modules#automocking-algorithm). ### vi.unmock @@ -420,8 +420,8 @@ This section describes how to work with [method mocks](/api/mock) and replace en function fn(fn?: Procedure | Constructable): Mock ``` -Creates a spy on a function, though can be initiated without one. Every time a function is invoked, it stores its call arguments, returns, and instances. Also, you can manipulate its behavior with [methods](/api/mock). -If no function is given, mock will return `undefined`, when invoked. +Creates a spy on a function, but can also be initiated without one. Every time a function is invoked, it stores its call arguments, returns, and instances. Additionally, you can manipulate its behavior with [methods](/api/mock). +If no function is given, mock will return `undefined` when invoked. ```ts const getApples = vi.fn(() => 0) @@ -524,7 +524,7 @@ This restores all original implementations on spies created with [`vi.spyOn`](#v After the mock was restored, you can spy on it again. ::: warning -This method also does not affect mocks created during [automocking](/guide/mocking-modules#mocking-a-module). +This method also does not affect mocks created during [automocking](/guide/mocking/modules#mocking-a-module). Note that unlike [`mock.mockRestore`](/api/mock#mockrestore), `vi.restoreAllMocks` will not clear mock history or reset the mock implementation ::: @@ -768,7 +768,7 @@ IntersectionObserver === undefined ## Fake Timers -This sections describes how to work with [fake timers](/guide/mocking#timers). +This sections describes how to work with [fake timers](/guide/mocking/timers). ### vi.advanceTimersByTime diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 6bdf43ca9..adb543221 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -241,6 +241,7 @@ If you need to run some tests using Node-based runner, you can define a [`projec ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' +import { playwright } from '@vitest/browser/providers/playwright' export default defineConfig({ test: { @@ -268,6 +269,7 @@ export default defineConfig({ name: 'browser', browser: { enabled: true, + provider: playwright(), instances: [ { browser: 'chromium' }, ], diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index cedd61f3d..5c17c73b5 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -341,13 +341,6 @@ Set to true to exit if port is already in use, instead of automatically trying t Provider used to run browser tests. Some browsers are only available for specific providers. Can be "webdriverio", "playwright", "preview", or the path to a custom provider. Visit [`browser.provider`](https://vitest.dev/guide/browser/config.html#browser-provider) for more information (default: `"preview"`) -### browser.providerOptions - -- **CLI:** `--browser.providerOptions ` -- **Config:** [browser.providerOptions](/guide/browser/config#browser-provideroptions) - -Options that are passed down to a browser provider. Visit [`browser.providerOptions`](https://vitest.dev/config/#browser-provideroptions) for more information - ### browser.isolate - **CLI:** `--browser.isolate` diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 84540085f..a22260d06 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -1,5 +1,6 @@ --- title: Mocking | Guide +outline: false --- # Mocking @@ -12,555 +13,21 @@ Always remember to clear or restore mocks before or after each test run to undo If you are not familiar with `vi.fn`, `vi.mock` or `vi.spyOn` methods, check the [API section](/api/vi) first. -## Dates +Vitest has a comprehensive list of guides regarding mocking: -Sometimes you need to be in control of the date to ensure consistency when testing. Vitest uses [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers) package for manipulating timers, as well as system date. You can find more about the specific API in detail [here](/api/vi#vi-setsystemtime). +- [Mocking Classes](/guide/mocking/classes.md) +- [Mocking Dates](/guide/mocking/dates.md) +- [Mocking the File System](/guide/mocking/file-system.md) +- [Mocking Functions](/guide/mocking/functions.md) +- [Mocking Globals](/guide/mocking/globals.md) +- [Mocking Modules](/guide/mocking/modules.md) +- [Mocking Requests](/guide/mocking/requests.md) +- [Mocking Timers](/guide/mocking/timers.md) -### Example - -```js -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -const businessHours = [9, 17] - -function purchase() { - const currentHour = new Date().getHours() - const [open, close] = businessHours - - if (currentHour > open && currentHour < close) { - return { message: 'Success' } - } - - return { message: 'Error' } -} - -describe('purchasing flow', () => { - beforeEach(() => { - // tell vitest we use mocked time - vi.useFakeTimers() - }) - - afterEach(() => { - // restoring date after each test run - vi.useRealTimers() - }) - - it('allows purchases within business hours', () => { - // set hour within business hours - const date = new Date(2000, 1, 1, 13) - vi.setSystemTime(date) - - // access Date.now() will result in the date set above - expect(purchase()).toEqual({ message: 'Success' }) - }) - - it('disallows purchases outside of business hours', () => { - // set hour outside business hours - const date = new Date(2000, 1, 1, 19) - vi.setSystemTime(date) - - // access Date.now() will result in the date set above - expect(purchase()).toEqual({ message: 'Error' }) - }) -}) -``` - -## Functions - -Mocking functions can be split up into two different categories; *spying & mocking*. - -Sometimes all you need is to validate whether or not a specific function has been called (and possibly which arguments were passed). In these cases a spy would be all we need which you can use directly with `vi.spyOn()` ([read more here](/api/vi#vi-spyon)). - -However spies can only help you **spy** on functions, they are not able to alter the implementation of those functions. In the case where we do need to create a fake (or mocked) version of a function we can use `vi.fn()` ([read more here](/api/vi#vi-fn)). - -We use [Tinyspy](https://github.com/tinylibs/tinyspy) as a base for mocking functions, but we have our own wrapper to make it `jest` compatible. Both `vi.fn()` and `vi.spyOn()` share the same methods, however only the return result of `vi.fn()` is callable. - -### Example - -```js -import { afterEach, describe, expect, it, vi } from 'vitest' - -const messages = { - items: [ - { message: 'Simple test message', from: 'Testman' }, - // ... - ], - getLatest, // can also be a `getter or setter if supported` -} - -function getLatest(index = messages.items.length - 1) { - return messages.items[index] -} - -describe('reading messages', () => { - afterEach(() => { - vi.restoreAllMocks() - }) - - it('should get the latest message with a spy', () => { - const spy = vi.spyOn(messages, 'getLatest') - expect(spy.getMockName()).toEqual('getLatest') - - expect(messages.getLatest()).toEqual( - messages.items[messages.items.length - 1], - ) - - expect(spy).toHaveBeenCalledTimes(1) - - spy.mockImplementationOnce(() => 'access-restricted') - expect(messages.getLatest()).toEqual('access-restricted') - - expect(spy).toHaveBeenCalledTimes(2) - }) - - it('should get with a mock', () => { - const mock = vi.fn().mockImplementation(getLatest) - - expect(mock()).toEqual(messages.items[messages.items.length - 1]) - expect(mock).toHaveBeenCalledTimes(1) - - mock.mockImplementationOnce(() => 'access-restricted') - expect(mock()).toEqual('access-restricted') - - expect(mock).toHaveBeenCalledTimes(2) - - expect(mock()).toEqual(messages.items[messages.items.length - 1]) - expect(mock).toHaveBeenCalledTimes(3) - }) -}) -``` - -### More - -- [Jest's Mock Functions](https://jestjs.io/docs/mock-function-api) - -## Globals - -You can mock global variables that are not present with `jsdom` or `node` by using [`vi.stubGlobal`](/api/vi#vi-stubglobal) helper. It will put the value of the global variable into a `globalThis` object. - -```ts -import { vi } from 'vitest' - -const IntersectionObserverMock = vi.fn(() => ({ - disconnect: vi.fn(), - observe: vi.fn(), - takeRecords: vi.fn(), - unobserve: vi.fn(), -})) - -vi.stubGlobal('IntersectionObserver', IntersectionObserverMock) - -// now you can access it as `IntersectionObserver` or `window.IntersectionObserver` -``` - -## Modules - -See ["Mocking Modules" guide](/guide/mocking-modules). - -## File System - -Mocking the file system ensures that the tests do not depend on the actual file system, making the tests more reliable and predictable. This isolation helps in avoiding side effects from previous tests. It allows for testing error conditions and edge cases that might be difficult or impossible to replicate with an actual file system, such as permission issues, disk full scenarios, or read/write errors. - -Vitest doesn't provide any file system mocking API out of the box. You can use `vi.mock` to mock the `fs` module manually, but it's hard to maintain. Instead, we recommend using [`memfs`](https://www.npmjs.com/package/memfs) to do that for you. `memfs` creates an in-memory file system, which simulates file system operations without touching the actual disk. This approach is fast and safe, avoiding any potential side effects on the real file system. - -### Example - -To automatically redirect every `fs` call to `memfs`, you can create `__mocks__/fs.cjs` and `__mocks__/fs/promises.cjs` files at the root of your project: - -::: code-group -```ts [__mocks__/fs.cjs] -// we can also use `import`, but then -// every export should be explicitly defined - -const { fs } = require('memfs') -module.exports = fs -``` - -```ts [__mocks__/fs/promises.cjs] -// we can also use `import`, but then -// every export should be explicitly defined - -const { fs } = require('memfs') -module.exports = fs.promises -``` -::: - -```ts [read-hello-world.js] -import { readFileSync } from 'node:fs' - -export function readHelloWorld(path) { - return readFileSync(path, 'utf-8') -} -``` - -```ts [hello-world.test.js] -import { beforeEach, expect, it, vi } from 'vitest' -import { fs, vol } from 'memfs' -import { readHelloWorld } from './read-hello-world.js' - -// tell vitest to use fs mock from __mocks__ folder -// this can be done in a setup file if fs should always be mocked -vi.mock('node:fs') -vi.mock('node:fs/promises') - -beforeEach(() => { - // reset the state of in-memory fs - vol.reset() -}) - -it('should return correct text', () => { - const path = '/hello-world.txt' - fs.writeFileSync(path, 'hello world') - - const text = readHelloWorld(path) - expect(text).toBe('hello world') -}) - -it('can return a value multiple times', () => { - // you can use vol.fromJSON to define several files - vol.fromJSON( - { - './dir1/hw.txt': 'hello dir1', - './dir2/hw.txt': 'hello dir2', - }, - // default cwd - '/tmp', - ) - - expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1') - expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2') -}) -``` - -## Requests - -Because Vitest runs in Node, mocking network requests is tricky; web APIs are not available, so we need something that will mimic network behavior for us. We recommend [Mock Service Worker](https://mswjs.io/) to accomplish this. It allows you to mock `http`, `WebSocket` and `GraphQL` network requests, and is framework agnostic. - -Mock Service Worker (MSW) works by intercepting the requests your tests make, allowing you to use it without changing any of your application code. In-browser, this uses the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). In Node.js, and for Vitest, it uses the [`@mswjs/interceptors`](https://github.com/mswjs/interceptors) library. To learn more about MSW, read their [introduction](https://mswjs.io/docs/) - -### Configuration - -You can use it like below in your [setup file](/config/#setupfiles) - -::: code-group - -```js [HTTP Setup] -import { afterAll, afterEach, beforeAll } from 'vitest' -import { setupServer } from 'msw/node' -import { http, HttpResponse } from 'msw' - -const posts = [ - { - userId: 1, - id: 1, - title: 'first post title', - body: 'first post body', - }, - // ... -] - -export const restHandlers = [ - http.get('https://rest-endpoint.example/path/to/posts', () => { - return HttpResponse.json(posts) - }), -] - -const server = setupServer(...restHandlers) - -// Start server before all tests -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) - -// Close server after all tests -afterAll(() => server.close()) - -// Reset handlers after each test for test isolation -afterEach(() => server.resetHandlers()) -``` - -```js [GraphQL Setup] -import { afterAll, afterEach, beforeAll } from 'vitest' -import { setupServer } from 'msw/node' -import { graphql, HttpResponse } from 'msw' - -const posts = [ - { - userId: 1, - id: 1, - title: 'first post title', - body: 'first post body', - }, - // ... -] - -const graphqlHandlers = [ - graphql.query('ListPosts', () => { - return HttpResponse.json({ - data: { posts }, - }) - }), -] - -const server = setupServer(...graphqlHandlers) - -// Start server before all tests -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) - -// Close server after all tests -afterAll(() => server.close()) - -// Reset handlers after each test for test isolation -afterEach(() => server.resetHandlers()) -``` - -```js [WebSocket Setup] -import { afterAll, afterEach, beforeAll } from 'vitest' -import { setupServer } from 'msw/node' -import { ws } from 'msw' - -const chat = ws.link('wss://chat.example.com') - -const wsHandlers = [ - chat.addEventListener('connection', ({ client }) => { - client.addEventListener('message', (event) => { - console.log('Received message from client:', event.data) - // Echo the received message back to the client - client.send(`Server received: ${event.data}`) - }) - }), -] - -const server = setupServer(...wsHandlers) - -// Start server before all tests -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) - -// Close server after all tests -afterAll(() => server.close()) - -// Reset handlers after each test for test isolation -afterEach(() => server.resetHandlers()) -``` -::: - -> Configuring the server with `onUnhandledRequest: 'error'` ensures that an error is thrown whenever there is a request that does not have a corresponding request handler. - -### More -There is much more to MSW. You can access cookies and query parameters, define mock error responses, and much more! To see all you can do with MSW, read [their documentation](https://mswjs.io/docs). - -## Timers - -When we test code that involves timeouts or intervals, instead of having our tests wait it out or timeout, we can speed up our tests by using "fake" timers that mock calls to `setTimeout` and `setInterval`. - -See the [`vi.useFakeTimers` API section](/api/vi#vi-usefaketimers) for a more in depth detailed API description. - -### Example - -```js -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -function executeAfterTwoHours(func) { - setTimeout(func, 1000 * 60 * 60 * 2) // 2 hours -} - -function executeEveryMinute(func) { - setInterval(func, 1000 * 60) // 1 minute -} - -const mock = vi.fn(() => console.log('executed')) - -describe('delayed execution', () => { - beforeEach(() => { - vi.useFakeTimers() - }) - afterEach(() => { - vi.restoreAllMocks() - }) - it('should execute the function', () => { - executeAfterTwoHours(mock) - vi.runAllTimers() - expect(mock).toHaveBeenCalledTimes(1) - }) - it('should not execute the function', () => { - executeAfterTwoHours(mock) - // advancing by 2ms won't trigger the func - vi.advanceTimersByTime(2) - expect(mock).not.toHaveBeenCalled() - }) - it('should execute every minute', () => { - executeEveryMinute(mock) - vi.advanceTimersToNextTimer() - expect(mock).toHaveBeenCalledTimes(1) - vi.advanceTimersToNextTimer() - expect(mock).toHaveBeenCalledTimes(2) - }) -}) -``` - -## Classes - -You can mock an entire class with a single `vi.fn` call. - -```ts -class Dog { - name: string - - constructor(name: string) { - this.name = name - } - - static getType(): string { - return 'animal' - } - - greet = (): string => { - return `Hi! My name is ${this.name}!` - } - - speak(): string { - return 'bark!' - } - - isHungry() {} - feed() {} -} -``` - -We can re-create this class with `vi.fn` (or `vi.spyOn().mockImplementation()`): - -```ts -const Dog = vi.fn(class { - static getType = vi.fn(() => 'mocked animal') - - constructor(name) { - this.name = name - } - - greet = vi.fn(() => `Hi! My name is ${this.name}!`) - speak = vi.fn(() => 'loud bark!') - feed = vi.fn() -}) -``` - -::: warning -If a non-primitive is returned from the constructor function, that value will become the result of the new expression. In this case the `[[Prototype]]` may not be correctly bound: - -```ts -const CorrectDogClass = vi.fn(function (name) { - this.name = name -}) - -const IncorrectDogClass = vi.fn(name => ({ - name -})) - -const Marti = new CorrectDogClass('Marti') -const Newt = new IncorrectDogClass('Newt') - -Marti instanceof CorrectDogClass // ✅ true -Newt instanceof IncorrectDogClass // ❌ false! -``` - -If you are mocking classes, prefer the class syntax over the function. -::: - -::: tip WHEN TO USE? -Generally speaking, you would re-create a class like this inside the module factory if the class is re-exported from another module: - -```ts -import { Dog } from './dog.js' - -vi.mock(import('./dog.js'), () => { - const Dog = vi.fn(class { - feed = vi.fn() - // ... other mocks - }) - return { Dog } -}) -``` - -This method can also be used to pass an instance of a class to a function that accepts the same interface: - -```ts [src/feed.ts] -function feed(dog: Dog) { - // ... -} -``` -```ts [tests/dog.test.ts] -import { expect, test, vi } from 'vitest' -import { feed } from '../src/feed.js' - -const Dog = vi.fn(class { - feed = vi.fn() -}) - -test('can feed dogs', () => { - const dogMax = new Dog('Max') - - feed(dogMax) - - expect(dogMax.feed).toHaveBeenCalled() - expect(dogMax.isHungry()).toBe(false) -}) -``` -::: - -Now, when we create a new instance of the `Dog` class its `speak` method (alongside `feed` and `greet`) is already mocked: - -```ts -const Cooper = new Dog('Cooper') -Cooper.speak() // loud bark! -Cooper.greet() // Hi! My name is Cooper! - -// you can use built-in assertions to check the validity of the call -expect(Cooper.speak).toHaveBeenCalled() -expect(Cooper.greet).toHaveBeenCalled() - -const Max = new Dog('Max') - -// methods are not shared between instances if you assigned them directly -expect(Max.speak).not.toHaveBeenCalled() -expect(Max.greet).not.toHaveBeenCalled() -``` - -We can reassign the return value for a specific instance: - -```ts -const dog = new Dog('Cooper') - -// "vi.mocked" is a type helper, since -// TypeScript doesn't know that Dog is a mocked class, -// it wraps any function in a Mock type -// without validating if the function is a mock -vi.mocked(dog.speak).mockReturnValue('woof woof') - -dog.speak() // woof woof -``` - -To mock the property, we can use the `vi.spyOn(dog, 'name', 'get')` method. This makes it possible to use spy assertions on the mocked property: - -```ts -const dog = new Dog('Cooper') - -const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max') - -expect(dog.name).toBe('Max') -expect(nameSpy).toHaveBeenCalledTimes(1) -``` - -::: tip -You can also spy on getters and setters using the same method. -::: - -::: danger -Using classes with `vi.fn()` was introduced in Vitest 4. Previously, you had to use `function` and `prototype` inheritence directly. See [v3 guide](https://v3.vitest.dev/guide/mocking.html#classes). -::: +For a simpler and quicker way to get started with mocking, you can check the Cheat Sheet below. ## Cheat Sheet -:::info -`vi` in the examples below is imported directly from `vitest`. You can also use it globally, if you set `globals` to `true` in your [config](/config/). -::: - I want to… ### Mock exported variables @@ -622,33 +89,16 @@ vi.mock(import('./example.js'), () => { }) return { SomeClass } }) -// SomeClass.mock.instances will have SomeClass ``` -2. Example with `vi.mock` and `.prototype`: -```ts [example.js] -export class SomeClass {} -``` -```ts -import { SomeClass } from './example.js' - -vi.mock(import('./example.js'), () => { - const SomeClass = vi.fn() - SomeClass.prototype.someMethod = vi.fn() - return { SomeClass } -}) -// SomeClass.mock.instances will have SomeClass -``` - -3. Example with `vi.spyOn`: +2. Example with `vi.spyOn`: ```ts import * as mod from './example.js' -const SomeClass = vi.fn() -SomeClass.prototype.someMethod = vi.fn() - -vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass) +vi.spyOn(mod, 'SomeClass').mockImplementation(class FakeClass { + someMethod = vi.fn() +}) ``` ::: warning diff --git a/docs/guide/mocking/classes.md b/docs/guide/mocking/classes.md new file mode 100644 index 000000000..abfebc77c --- /dev/null +++ b/docs/guide/mocking/classes.md @@ -0,0 +1,158 @@ +# Mocking Classes + +You can mock an entire class with a single [`vi.fn`](/api/vi#fn) call. + +```ts +class Dog { + name: string + + constructor(name: string) { + this.name = name + } + + static getType(): string { + return 'animal' + } + + greet = (): string => { + return `Hi! My name is ${this.name}!` + } + + speak(): string { + return 'bark!' + } + + isHungry() {} + feed() {} +} +``` + +We can re-create this class with `vi.fn` (or `vi.spyOn().mockImplementation()`): + +```ts +const Dog = vi.fn(class { + static getType = vi.fn(() => 'mocked animal') + + constructor(name) { + this.name = name + } + + greet = vi.fn(() => `Hi! My name is ${this.name}!`) + speak = vi.fn(() => 'loud bark!') + feed = vi.fn() +}) +``` + +::: warning +If a non-primitive is returned from the constructor function, that value will become the result of the new expression. In this case the `[[Prototype]]` may not be correctly bound: + +```ts +const CorrectDogClass = vi.fn(function (name) { + this.name = name +}) + +const IncorrectDogClass = vi.fn(name => ({ + name +})) + +const Marti = new CorrectDogClass('Marti') +const Newt = new IncorrectDogClass('Newt') + +Marti instanceof CorrectDogClass // ✅ true +Newt instanceof IncorrectDogClass // ❌ false! +``` + +If you are mocking classes, prefer the class syntax over the function. +::: + +::: tip WHEN TO USE? +Generally speaking, you would re-create a class like this inside the module factory if the class is re-exported from another module: + +```ts +import { Dog } from './dog.js' + +vi.mock(import('./dog.js'), () => { + const Dog = vi.fn(class { + feed = vi.fn() + // ... other mocks + }) + return { Dog } +}) +``` + +This method can also be used to pass an instance of a class to a function that accepts the same interface: + +```ts [src/feed.ts] +function feed(dog: Dog) { + // ... +} +``` +```ts [tests/dog.test.ts] +import { expect, test, vi } from 'vitest' +import { feed } from '../src/feed.js' + +const Dog = vi.fn(class { + feed = vi.fn() +}) + +test('can feed dogs', () => { + const dogMax = new Dog('Max') + + feed(dogMax) + + expect(dogMax.feed).toHaveBeenCalled() + expect(dogMax.isHungry()).toBe(false) +}) +``` +::: + +Now, when we create a new instance of the `Dog` class its `speak` method (alongside `feed` and `greet`) is already mocked: + +```ts +const Cooper = new Dog('Cooper') +Cooper.speak() // loud bark! +Cooper.greet() // Hi! My name is Cooper! + +// you can use built-in assertions to check the validity of the call +expect(Cooper.speak).toHaveBeenCalled() +expect(Cooper.greet).toHaveBeenCalled() + +const Max = new Dog('Max') + +// methods are not shared between instances if you assigned them directly +expect(Max.speak).not.toHaveBeenCalled() +expect(Max.greet).not.toHaveBeenCalled() +``` + +We can reassign the return value for a specific instance: + +```ts +const dog = new Dog('Cooper') + +// "vi.mocked" is a type helper, since +// TypeScript doesn't know that Dog is a mocked class, +// it wraps any function in a Mock type +// without validating if the function is a mock +vi.mocked(dog.speak).mockReturnValue('woof woof') + +dog.speak() // woof woof +``` + +To mock the property, we can use the `vi.spyOn(dog, 'name', 'get')` method. This makes it possible to use spy assertions on the mocked property: + +```ts +const dog = new Dog('Cooper') + +const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max') + +expect(dog.name).toBe('Max') +expect(nameSpy).toHaveBeenCalledTimes(1) +``` + +::: tip +You can also spy on getters and setters using the same method. +::: + +::: danger +Using classes with `vi.fn()` was introduced in Vitest 4. Previously, you had to use `function` and `prototype` inheritence directly. See [v3 guide](https://v3.vitest.dev/guide/mocking.html#classes). +::: diff --git a/docs/guide/mocking/dates.md b/docs/guide/mocking/dates.md new file mode 100644 index 000000000..f021556ed --- /dev/null +++ b/docs/guide/mocking/dates.md @@ -0,0 +1,52 @@ +# Mocking Dates + +Sometimes you need to be in control of the date to ensure consistency when testing. Vitest uses [`@sinonjs/fake-timers`](https://github.com/sinonjs/fake-timers) package for manipulating timers, as well as system date. You can find more about the specific API in detail [here](/api/vi#vi-setsystemtime). + +## Example + +```js +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const businessHours = [9, 17] + +function purchase() { + const currentHour = new Date().getHours() + const [open, close] = businessHours + + if (currentHour > open && currentHour < close) { + return { message: 'Success' } + } + + return { message: 'Error' } +} + +describe('purchasing flow', () => { + beforeEach(() => { + // tell vitest we use mocked time + vi.useFakeTimers() + }) + + afterEach(() => { + // restoring date after each test run + vi.useRealTimers() + }) + + it('allows purchases within business hours', () => { + // set hour within business hours + const date = new Date(2000, 1, 1, 13) + vi.setSystemTime(date) + + // access Date.now() will result in the date set above + expect(purchase()).toEqual({ message: 'Success' }) + }) + + it('disallows purchases outside of business hours', () => { + // set hour outside business hours + const date = new Date(2000, 1, 1, 19) + vi.setSystemTime(date) + + // access Date.now() will result in the date set above + expect(purchase()).toEqual({ message: 'Error' }) + }) +}) +``` diff --git a/docs/guide/mocking/file-system.md b/docs/guide/mocking/file-system.md new file mode 100644 index 000000000..a8be3df08 --- /dev/null +++ b/docs/guide/mocking/file-system.md @@ -0,0 +1,74 @@ +# Mocking the File System + +Mocking the file system ensures that the tests do not depend on the actual file system, making the tests more reliable and predictable. This isolation helps in avoiding side effects from previous tests. It allows for testing error conditions and edge cases that might be difficult or impossible to replicate with an actual file system, such as permission issues, disk full scenarios, or read/write errors. + +Vitest doesn't provide any file system mocking API out of the box. You can use `vi.mock` to mock the `fs` module manually, but it's hard to maintain. Instead, we recommend using [`memfs`](https://www.npmjs.com/package/memfs) to do that for you. `memfs` creates an in-memory file system, which simulates file system operations without touching the actual disk. This approach is fast and safe, avoiding any potential side effects on the real file system. + +## Example + +To automatically redirect every `fs` call to `memfs`, you can create `__mocks__/fs.cjs` and `__mocks__/fs/promises.cjs` files at the root of your project: + +::: code-group +```ts [__mocks__/fs.cjs] +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs') +module.exports = fs +``` + +```ts [__mocks__/fs/promises.cjs] +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs') +module.exports = fs.promises +``` +::: + +```ts [read-hello-world.js] +import { readFileSync } from 'node:fs' + +export function readHelloWorld(path) { + return readFileSync(path, 'utf-8') +} +``` + +```ts [hello-world.test.js] +import { beforeEach, expect, it, vi } from 'vitest' +import { fs, vol } from 'memfs' +import { readHelloWorld } from './read-hello-world.js' + +// tell vitest to use fs mock from __mocks__ folder +// this can be done in a setup file if fs should always be mocked +vi.mock('node:fs') +vi.mock('node:fs/promises') + +beforeEach(() => { + // reset the state of in-memory fs + vol.reset() +}) + +it('should return correct text', () => { + const path = '/hello-world.txt' + fs.writeFileSync(path, 'hello world') + + const text = readHelloWorld(path) + expect(text).toBe('hello world') +}) + +it('can return a value multiple times', () => { + // you can use vol.fromJSON to define several files + vol.fromJSON( + { + './dir1/hw.txt': 'hello dir1', + './dir2/hw.txt': 'hello dir2', + }, + // default cwd + '/tmp', + ) + + expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1') + expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2') +}) +``` diff --git a/docs/guide/mocking/functions.md b/docs/guide/mocking/functions.md new file mode 100644 index 000000000..58c729911 --- /dev/null +++ b/docs/guide/mocking/functions.md @@ -0,0 +1,61 @@ +# Mocking Functions + +Mocking functions can be split up into two different categories: spying and mocking. + +If you need to observe the behaviour of a method on an object, you can use [`vi.spyOn()`](/api/vi#vi-spyon) to create a spy that tracks calls to that method. + +If you need to pass down a custom function implementation as an argument or create a new mocked entity, you can use [`vi.fn()`](/api/vi#vi-fn) to create a mock function. + +Both `vi.spyOn` and `vi.fn` share the same methods. + +## Example + +```js +import { afterEach, describe, expect, it, vi } from 'vitest' + +const messages = { + items: [ + { message: 'Simple test message', from: 'Testman' }, + // ... + ], + addItem(item) { + messages.items.push(item) + messages.callbacks.forEach(callback => callback(item)) + }, + onItem(callback) { + messages.callbacks.push(callback) + }, + getLatest, // can also be a `getter or setter if supported` +} + +function getLatest(index = messages.items.length - 1) { + return messages.items[index] +} + +it('should get the latest message with a spy', () => { + const spy = vi.spyOn(messages, 'getLatest') + expect(spy.getMockName()).toEqual('getLatest') + + expect(messages.getLatest()).toEqual( + messages.items[messages.items.length - 1], + ) + + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockImplementationOnce(() => 'access-restricted') + expect(messages.getLatest()).toEqual('access-restricted') + + expect(spy).toHaveBeenCalledTimes(2) +}) + +it('passing down the mock', () => { + const callback = vi.fn() + messages.onItem(callback) + + messages.addItem({ message: 'Another test message', from: 'Testman' }) + expect(callback).toHaveBeenCalledWith({ + message: 'Another test message', + from: 'Testman', + }) +}) +``` diff --git a/docs/guide/mocking/globals.md b/docs/guide/mocking/globals.md new file mode 100644 index 000000000..fe195628c --- /dev/null +++ b/docs/guide/mocking/globals.md @@ -0,0 +1,20 @@ +# Mocking Globals + +You can mock global variables that are not present with `jsdom` or `node` by using [`vi.stubGlobal`](/api/vi#vi-stubglobal) helper. It will put the value of the global variable into a `globalThis` object. + +By default, Vitest does not reset these globals, but you can turn on the [`unstubGlobals`](/config/#unstubglobals) option in your config to restore the original values after each test or call [`vi.unstubAllGlobals()`](/api/vi#vi-unstuballglobals) manually. + +```ts +import { vi } from 'vitest' + +const IntersectionObserverMock = vi.fn(() => ({ + disconnect: vi.fn(), + observe: vi.fn(), + takeRecords: vi.fn(), + unobserve: vi.fn(), +})) + +vi.stubGlobal('IntersectionObserver', IntersectionObserverMock) + +// now you can access it as `IntersectionObserver` or `window.IntersectionObserver` +``` diff --git a/docs/guide/mocking-modules.md b/docs/guide/mocking/modules.md similarity index 99% rename from docs/guide/mocking-modules.md rename to docs/guide/mocking/modules.md index 83b7d54b6..9a0a29ef4 100644 --- a/docs/guide/mocking-modules.md +++ b/docs/guide/mocking/modules.md @@ -155,6 +155,8 @@ vi.mock(import('./example.js')) If the file `./__mocks__/example.js` exists, then Vitest will load it instead. Otherwise, Vitest will load the original module and replace everything recursively: +{#automocking-algorithm} + - All arrays will be empty - All primitives will stay untouched - All getters will return `undefined` diff --git a/docs/guide/mocking/requests.md b/docs/guide/mocking/requests.md new file mode 100644 index 000000000..f6e532ecb --- /dev/null +++ b/docs/guide/mocking/requests.md @@ -0,0 +1,114 @@ +# Mocking Requests + +Because Vitest runs in Node, mocking network requests is tricky; web APIs are not available, so we need something that will mimic network behavior for us. We recommend [Mock Service Worker](https://mswjs.io/) to accomplish this. It allows you to mock `http`, `WebSocket` and `GraphQL` network requests, and is framework agnostic. + +Mock Service Worker (MSW) works by intercepting the requests your tests make, allowing you to use it without changing any of your application code. In-browser, this uses the [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API). In Node.js, and for Vitest, it uses the [`@mswjs/interceptors`](https://github.com/mswjs/interceptors) library. To learn more about MSW, read their [introduction](https://mswjs.io/docs/) + +## Configuration + +You can use it like below in your [setup file](/config/#setupfiles) + +::: code-group + +```js [HTTP Setup] +import { afterAll, afterEach, beforeAll } from 'vitest' +import { setupServer } from 'msw/node' +import { http, HttpResponse } from 'msw' + +const posts = [ + { + userId: 1, + id: 1, + title: 'first post title', + body: 'first post body', + }, + // ... +] + +export const restHandlers = [ + http.get('https://rest-endpoint.example/path/to/posts', () => { + return HttpResponse.json(posts) + }), +] + +const server = setupServer(...restHandlers) + +// Start server before all tests +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +// Close server after all tests +afterAll(() => server.close()) + +// Reset handlers after each test for test isolation +afterEach(() => server.resetHandlers()) +``` + +```js [GraphQL Setup] +import { afterAll, afterEach, beforeAll } from 'vitest' +import { setupServer } from 'msw/node' +import { graphql, HttpResponse } from 'msw' + +const posts = [ + { + userId: 1, + id: 1, + title: 'first post title', + body: 'first post body', + }, + // ... +] + +const graphqlHandlers = [ + graphql.query('ListPosts', () => { + return HttpResponse.json({ + data: { posts }, + }) + }), +] + +const server = setupServer(...graphqlHandlers) + +// Start server before all tests +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +// Close server after all tests +afterAll(() => server.close()) + +// Reset handlers after each test for test isolation +afterEach(() => server.resetHandlers()) +``` + +```js [WebSocket Setup] +import { afterAll, afterEach, beforeAll } from 'vitest' +import { setupServer } from 'msw/node' +import { ws } from 'msw' + +const chat = ws.link('wss://chat.example.com') + +const wsHandlers = [ + chat.addEventListener('connection', ({ client }) => { + client.addEventListener('message', (event) => { + console.log('Received message from client:', event.data) + // Echo the received message back to the client + client.send(`Server received: ${event.data}`) + }) + }), +] + +const server = setupServer(...wsHandlers) + +// Start server before all tests +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +// Close server after all tests +afterAll(() => server.close()) + +// Reset handlers after each test for test isolation +afterEach(() => server.resetHandlers()) +``` +::: + +> Configuring the server with `onUnhandledRequest: 'error'` ensures that an error is thrown whenever there is a request that does not have a corresponding request handler. + +## More +There is much more to MSW. You can access cookies and query parameters, define mock error responses, and much more! To see all you can do with MSW, read [their documentation](https://mswjs.io/docs). diff --git a/docs/guide/mocking/timers.md b/docs/guide/mocking/timers.md new file mode 100644 index 000000000..7caa9cc73 --- /dev/null +++ b/docs/guide/mocking/timers.md @@ -0,0 +1,48 @@ +# Timers + +When we test code that involves timeouts or intervals, instead of having our tests wait it out or timeout, we can speed up our tests by using "fake" timers that mock calls to `setTimeout` and `setInterval`. + +See the [`vi.useFakeTimers` API section](/api/vi#vi-usefaketimers) for a more in depth detailed API description. + +## Example + +```js +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +function executeAfterTwoHours(func) { + setTimeout(func, 1000 * 60 * 60 * 2) // 2 hours +} + +function executeEveryMinute(func) { + setInterval(func, 1000 * 60) // 1 minute +} + +const mock = vi.fn(() => console.log('executed')) + +describe('delayed execution', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should execute the function', () => { + executeAfterTwoHours(mock) + vi.runAllTimers() + expect(mock).toHaveBeenCalledTimes(1) + }) + it('should not execute the function', () => { + executeAfterTwoHours(mock) + // advancing by 2ms won't trigger the func + vi.advanceTimersByTime(2) + expect(mock).not.toHaveBeenCalled() + }) + it('should execute every minute', () => { + executeEveryMinute(mock) + vi.advanceTimersToNextTimer() + expect(mock).toHaveBeenCalledTimes(1) + vi.advanceTimersToNextTimer() + expect(mock).toHaveBeenCalledTimes(2) + }) +}) +``` diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 399cb86e4..8ebb8c815 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -192,7 +192,7 @@ export interface VitestUtils { * The call to `vi.mock` is hoisted to the top of the file, so you don't have access to variables declared in the global file scope * unless they are defined with [`vi.hoisted`](https://vitest.dev/api/vi#vi-hoisted) before this call. * - * Mocking algorithm is described in [documentation](https://vitest.dev/guide/mocking#modules). + * 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 */ @@ -217,7 +217,7 @@ export interface VitestUtils { * * Unlike [`vi.mock`](https://vitest.dev/api/vi#vi-mock), this method will not mock statically imported modules because it is not hoisted to the top of the file. * - * Mocking algorithm is described in [documentation](https://vitest.dev/guide/mocking#modules). + * 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 */ @@ -254,7 +254,7 @@ export interface VitestUtils { /** * Imports a module with all of its properties and nested properties mocked. * - * Mocking algorithm is described in [documentation](https://vitest.dev/guide/mocking#modules). + * Mocking algorithm is described in [documentation](https://vitest.dev/guide/mocking/modules). * @example * ```ts * const example = await vi.importMock('./example.js')