From a820b15665a0952e3971c5ce8875cc050b30f4ce Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 2 Jul 2024 13:30:25 +0200 Subject: [PATCH] docs: add mock fs section (#6021) --- docs/guide/mocking.md | 77 +++++++++++++++++++ packages/vitest/src/runtime/execute.ts | 4 +- .../vitest/src/runtime/external-executor.ts | 4 +- packages/vitest/src/runtime/mocker.ts | 4 +- packages/vitest/src/runtime/vm/file-map.ts | 6 +- packages/web-worker/src/utils.ts | 5 +- 6 files changed, 94 insertions(+), 6 deletions(-) diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index 424053ae6df2..604913bcc371 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -354,6 +354,83 @@ describe('get a list of todo items', () => { }) ``` +## 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 automatially 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 +// hello-world.js +import { readFileSync } from 'node:fs' + +export function readHelloWorld(path) { + return readFileSync('./hello-world.txt') +} +``` + +```ts +// hello-world.test.js +import { beforeEach, expect, it, vi } from 'vitest' +import { fs, vol } from 'memfs' +import { readHelloWorld } from './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 will let you mock both `REST` and `GraphQL` network requests, and is framework agnostic. diff --git a/packages/vitest/src/runtime/execute.ts b/packages/vitest/src/runtime/execute.ts index fa43260ff69b..1f2d73f2b4d2 100644 --- a/packages/vitest/src/runtime/execute.ts +++ b/packages/vitest/src/runtime/execute.ts @@ -1,6 +1,6 @@ import vm from 'node:vm' import { pathToFileURL } from 'node:url' -import { readFileSync } from 'node:fs' +import fs from 'node:fs' import type { ModuleCacheMap } from 'vite-node/client' import { DEFAULT_REQUEST_STUBS, ViteNodeRunner } from 'vite-node/client' import { @@ -18,6 +18,8 @@ import type { WorkerGlobalState } from '../types' import { VitestMocker } from './mocker' import type { ExternalModulesExecutor } from './external-executor' +const { readFileSync } = fs + export interface ExecuteOptions extends ViteNodeRunnerOptions { mockMap: MockMap moduleDirectories?: string[] diff --git a/packages/vitest/src/runtime/external-executor.ts b/packages/vitest/src/runtime/external-executor.ts index c7650cda41cb..cf432efea45f 100644 --- a/packages/vitest/src/runtime/external-executor.ts +++ b/packages/vitest/src/runtime/external-executor.ts @@ -1,7 +1,7 @@ import vm from 'node:vm' import { fileURLToPath, pathToFileURL } from 'node:url' import { dirname } from 'node:path' -import { existsSync, statSync } from 'node:fs' +import fs from 'node:fs' import { extname, join, normalize } from 'pathe' import { getCachedData, isNodeBuiltin, setCacheData } from 'vite-node/utils' import type { RuntimeRPC } from '../types/rpc' @@ -14,6 +14,8 @@ import { ViteExecutor } from './vm/vite-executor' const SyntheticModule: typeof VMSyntheticModule = (vm as any).SyntheticModule +const { existsSync, statSync } = fs + // always defined when we use vm pool const nativeResolve = import.meta.resolve! diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index 5ac4cdbe9882..ddf245f5d8a2 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -1,4 +1,4 @@ -import { existsSync, readdirSync } from 'node:fs' +import fs from 'node:fs' import vm from 'node:vm' import { basename, dirname, extname, isAbsolute, join, resolve } from 'pathe' import { getType, highlight } from '@vitest/utils' @@ -8,6 +8,8 @@ import { getAllMockableProperties } from '../utils/base' import type { MockFactory, PendingSuiteMock } from '../types/mocker' import type { VitestExecutor } from './execute' +const { existsSync, readdirSync } = fs + const spyModulePath = resolve(distDir, 'spy.js') class RefTracker { diff --git a/packages/vitest/src/runtime/vm/file-map.ts b/packages/vitest/src/runtime/vm/file-map.ts index 2d89e0d205df..67ef774cf99d 100644 --- a/packages/vitest/src/runtime/vm/file-map.ts +++ b/packages/vitest/src/runtime/vm/file-map.ts @@ -1,4 +1,6 @@ -import { promises as fs, readFileSync } from 'node:fs' +import fs from 'node:fs' + +const { promises, readFileSync } = fs export class FileMap { private fsCache = new Map() @@ -9,7 +11,7 @@ export class FileMap { if (cached != null) { return cached } - const source = await fs.readFile(path, 'utf-8') + const source = await promises.readFile(path, 'utf-8') this.fsCache.set(path, source) return source } diff --git a/packages/web-worker/src/utils.ts b/packages/web-worker/src/utils.ts index 2d0be79c1e32..136d1068d91b 100644 --- a/packages/web-worker/src/utils.ts +++ b/packages/web-worker/src/utils.ts @@ -1,9 +1,12 @@ -import { readFileSync } from 'node:fs' +import { readFileSync as _readFileSync } from 'node:fs' import type { WorkerGlobalState } from 'vitest' import ponyfillStructuredClone from '@ungap/structured-clone' import createDebug from 'debug' import type { CloneOption } from './types' +// keep the reference in case it was mocked +const readFileSync = _readFileSync + export const debug = createDebug('vitest:web-worker') export function getWorkerState(): WorkerGlobalState {