Skip to content

Commit

Permalink
docs: add mock fs section (#6021)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored Jul 2, 2024
1 parent 368c137 commit a820b15
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 6 deletions.
77 changes: 77 additions & 0 deletions docs/guide/mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion packages/vitest/src/runtime/execute.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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[]
Expand Down
4 changes: 3 additions & 1 deletion packages/vitest/src/runtime/external-executor.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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!

Expand Down
4 changes: 3 additions & 1 deletion packages/vitest/src/runtime/mocker.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions packages/vitest/src/runtime/vm/file-map.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>()
Expand All @@ -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
}
Expand Down
5 changes: 4 additions & 1 deletion packages/web-worker/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit a820b15

Please sign in to comment.