Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,23 @@ generated as part of the test runner output. If no tests are run, a coverage
report is not generated. See the documentation on
[collecting code coverage from tests][] for more details.

### `--experimental-test-fs-mocks`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

Enable file system mocking in the test runner.

This feature allows tests to mock file system operations without actually
reading from or writing to the disk. By default, virtual files take precedence
but real file system operations are still allowed for paths not defined in
the virtual file system. Use the `isolate: true` option to completely isolate
tests from the real file system. See the documentation on
[mocking the file system][] for more details.

### `--experimental-test-module-mocks`

<!-- YAML
Expand Down Expand Up @@ -4234,6 +4251,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[global setup and teardown]: test.md#global-setup-and-teardown
[jitless]: https://v8.dev/blog/jitless
[libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html
[mocking the file system]: test.md#file-system
[module compile cache]: module.md#module-compile-cache
[preloading asynchronous module customization hooks]: module.md#registration-of-asynchronous-customization-hooks
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
Expand Down
335 changes: 335 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,156 @@ test('runs timers as setTime passes ticks', (context) => {
});
```

### File system

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development
Mocking the file system is a technique commonly used in software testing to
simulate file operations without actually writing to or reading from the disk.
This allows for safer, faster, and more predictable tests when working with
file system operations.

Refer to the [`MockFileSystem`][] class for a full list of methods and features.

**Note:** This feature requires the `--experimental-test-fs-mocks` flag.

The example below shows how to mock file system operations. Using
`.enable({ files: {...} })` it will mock the file system methods in the
[node:fs](./fs.md) and [node:fs/promises](./fs.md#promises-api) modules.

```mjs
import assert from 'node:assert';
import fs from 'node:fs';
import { test } from 'node:test';

test('mocks file system operations', (context) => {
// Enable file system mocking with virtual files
context.mock.fs.enable({
files: {
'/virtual/test.txt': 'Hello, World!',
'/virtual/data.json': '{"key": "value"}',
},
});

// Read virtual files
const content = fs.readFileSync('/virtual/test.txt', 'utf8');
assert.strictEqual(content, 'Hello, World!');

// Write to virtual file system
fs.writeFileSync('/virtual/new.txt', 'New content');
assert.strictEqual(fs.readFileSync('/virtual/new.txt', 'utf8'), 'New content');

// Check if virtual file exists
assert.strictEqual(fs.existsSync('/virtual/test.txt'), true);
});
```

```cjs
const assert = require('node:assert');
const fs = require('node:fs');
const { test } = require('node:test');

test('mocks file system operations', (context) => {
// Enable file system mocking with virtual files
context.mock.fs.enable({
files: {
'/virtual/test.txt': 'Hello, World!',
'/virtual/data.json': '{"key": "value"}',
},
});

// Read virtual files
const content = fs.readFileSync('/virtual/test.txt', 'utf8');
assert.strictEqual(content, 'Hello, World!');

// Write to virtual file system
fs.writeFileSync('/virtual/new.txt', 'New content');
assert.strictEqual(fs.readFileSync('/virtual/new.txt', 'utf8'), 'New content');

// Check if virtual file exists
assert.strictEqual(fs.existsSync('/virtual/test.txt'), true);
});
```

By default, the mock file system allows access to both virtual and real files,
with virtual files taking precedence. When file system mocking is enabled,
**all write operations go to the virtual file system**, regardless of whether
the path exists in the real file system. This prevents tests from accidentally
modifying the real file system.

You can enable isolation mode to completely isolate tests from the real file
system for read operations as well:

```mjs
import assert from 'node:assert';
import fs from 'node:fs';
import { test } from 'node:test';

test('complete file system isolation', (context) => {
context.mock.fs.enable({
files: {
'/virtual/only.txt': 'Only this file exists',
},
isolate: true, // Enable full isolation mode
});

// Virtual file works
assert.strictEqual(fs.readFileSync('/virtual/only.txt', 'utf8'), 'Only this file exists');

// Real files are not accessible
assert.throws(() => {
fs.readFileSync('/etc/passwd');
}, {
code: 'ENOENT',
});
});
```

```cjs
const assert = require('node:assert');
const fs = require('node:fs');
const { test } = require('node:test');

test('complete file system isolation', (context) => {
context.mock.fs.enable({
files: {
'/virtual/only.txt': 'Only this file exists',
},
isolate: true, // Enable full isolation mode
});

// Virtual file works
assert.strictEqual(fs.readFileSync('/virtual/only.txt', 'utf8'), 'Only this file exists');

// Real files are not accessible
assert.throws(() => {
fs.readFileSync('/etc/passwd');
}, {
code: 'ENOENT',
});
});
```

#### Windows path handling

On Windows, use appropriate path separators or forward slashes:

```js
test('Windows path handling', (t) => {
t.mock.fs.enable({
files: {
'C:/virtual/test.txt': 'Hello, World!',
// or using backslashes
// 'C:\\virtual\\test.txt': 'Hello, World!',
},
});
});
```

## Snapshot testing

<!-- YAML
Expand Down Expand Up @@ -3106,6 +3256,190 @@ test('runAll functions following the given order', (context) => {
});
```

## Class: `MockFileSystem`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> Stability: 1.0 - Early development
> Stability: 1.0 - Early development. Enable this API via ...

Mocking the file system allows tests to simulate file operations without
actually reading from or writing to the disk. This makes tests safer, faster,
and more predictable.

The [`MockTracker`][] provides a top-level `fs` export
which is a `MockFileSystem` instance.

**Note:** This class requires the `--experimental-test-fs-mocks` flag.

### `fs.enable([options])`

<!-- YAML
added: REPLACEME
-->

Enables file system mocking.

Comment on lines +3281 to +3283
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go after the typed list

* `options` {Object} Optional configuration options for enabling file system
mocking. The following properties are supported:
* `files` {Object} An object mapping file paths to their content. Content
can be a string, `Buffer`, or `Uint8Array`. Strings are automatically
converted to `Buffer` using UTF-8 encoding. **Default:** `{}`.
* `isolate` {boolean} If `true`, only virtual files are accessible and
any access to paths not in `files` will throw `ENOENT`. If `false`
(the default), virtual files take precedence but real file system
operations are still allowed for other paths. **Note:** When mocking is
enabled, write operations always go to the virtual file system regardless
of this setting. **Default:** `false`.
* `apis` {Array} An optional array specifying which fs API families to mock.
Each value mocks the synchronous, callback, and promise versions of that
API (e.g., `'readFile'` mocks `fs.readFileSync()`, `fs.readFile()`, and
`fsPromises.readFile()`). The supported values are `'readFile'`,
`'writeFile'`, `'appendFile'`, `'stat'`, `'lstat'`, `'access'`, `'exists'`,
`'unlink'`, `'mkdir'`, `'rmdir'`, and `'readdir'`. **Default:** all
supported APIs.

**Note:** When file system mocking is enabled, the mock automatically
creates parent directories for all virtual files.
Comment on lines +3303 to +3304
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Note:** When file system mocking is enabled, the mock automatically
creates parent directories for all virtual files.
When file system mocking is enabled, the mock automatically
creates parent directories for all virtual files.


Example usage:

```mjs
import { mock } from 'node:test';
import { Buffer } from 'node:buffer';

mock.fs.enable({
files: {
'/path/to/file.txt': 'file content',
'/path/to/binary.bin': Buffer.from([0x00, 0x01, 0x02]),
},
});
```

```cjs
const { mock } = require('node:test');

mock.fs.enable({
files: {
'/path/to/file.txt': 'file content',
'/path/to/binary.bin': Buffer.from([0x00, 0x01, 0x02]),
},
});
```

### `fs.reset()`

<!-- YAML
added: REPLACEME
-->

Restores the original file system functions and clears all virtual files.
This function is automatically called when a test using the mock file system
completes.

### Supported `fs` methods

The following methods are intercepted by the mock file system:

**Synchronous methods:**

* `fs.readFileSync()`
* `fs.writeFileSync()`
* `fs.appendFileSync()`
* `fs.statSync()`
* `fs.lstatSync()`
* `fs.existsSync()`
* `fs.accessSync()`
* `fs.unlinkSync()`
* `fs.mkdirSync()`
* `fs.rmdirSync()`
* `fs.readdirSync()`

**Callback methods:**

* `fs.readFile()`
* `fs.writeFile()`
* `fs.appendFile()`
* `fs.stat()`
* `fs.lstat()`
* `fs.exists()`
* `fs.access()`
* `fs.unlink()`
* `fs.mkdir()`
* `fs.rmdir()`
* `fs.readdir()`

**Promise methods (`fs/promises`):**

* `fsPromises.readFile()`
* `fsPromises.writeFile()`
* `fsPromises.appendFile()`
* `fsPromises.stat()`
* `fsPromises.lstat()`
* `fsPromises.access()`
* `fsPromises.unlink()`
* `fsPromises.mkdir()`
* `fsPromises.rmdir()`
* `fsPromises.readdir()`

### Limitations

The mock file system has the following limitations:

* **Symbolic links are not supported.** `lstat()` behaves identically to
`stat()`, and `isSymbolicLink()` always returns `false`.
* **Dirent objects are not `fs.Dirent` instances.** The objects returned by
`readdir({ withFileTypes: true })` have the same properties and methods as
`fs.Dirent`, but `dirent instanceof fs.Dirent` will return `false`.
* **The following methods are not mocked:**
* `fs.copyFile()` / `fs.copyFileSync()`
* `fs.rename()` / `fs.renameSync()`
* `fs.chmod()` / `fs.chmodSync()` / `fs.chown()` / `fs.chownSync()`
* `fs.realpath()` / `fs.realpathSync()`
* `fs.watch()` / `fs.watchFile()` / `fs.unwatchFile()`
* `fs.open()` / `fs.openSync()` and file descriptor operations
* `fs.createReadStream()` / `fs.createWriteStream()`
* **File permissions are not enforced.** All virtual files are created with
mode `0o644` and permission checks are not performed.
* **File descriptors are not supported.** Operations that require file
descriptors will not work with virtual files.
* **`mkdir()` return value.** When called with `{ recursive: true }`,
`mkdir()` returns the first directory path created (matching real `fs`
behavior). Without `recursive`, it returns `undefined`.
* **Recursive `readdir()` is not supported.** Calling `readdir()` with
`{ recursive: true }` will throw `ERR_INVALID_ARG_VALUE`.

### Stats object

Mock stats objects have the following properties with default values:

| Property | Type | Default |
| ------------------------------------------- | -------- | -------------------------------- |
| `dev` | `number` | `0` |
| `ino` | `number` | `0` |
| `mode` | `number` | File: `0o100644`, Dir: `0o40644` |
| `nlink` | `number` | `1` |
| `uid` | `number` | `0` |
| `gid` | `number` | `0` |
| `rdev` | `number` | `0` |
| `size` | `number` | Content length |
| `blksize` | `number` | `4096` |
| `blocks` | `number` | `ceil(size / 512)` |
| `atime`/`mtime`/`ctime`/`birthtime` | `Date` | Creation time |
| `atimeMs`/`mtimeMs`/`ctimeMs`/`birthtimeMs` | `number` | Creation time (ms) |
Comment on lines +3417 to +3430
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, and perhaps this is too much for a single PR, if these should be mock-able.

{
  files: {
    myFilePath: 'content',
    myOtherFile: { atime: 123, content: 'some data' },
  }
}


The stats object includes the following methods that return the expected
values for virtual files and directories:

* `isFile()`
* `isDirectory()`
* `isBlockDevice()` - always returns `false`
* `isCharacterDevice()` - always returns `false`
* `isSymbolicLink()` - always returns `false`
* `isFIFO()` - always returns `false`
* `isSocket()` - always returns `false`

## Class: `TestsStream`

<!-- YAML
Expand Down Expand Up @@ -4056,6 +4390,7 @@ Can be used to abort test subtasks when the test has been aborted.
[`--test-skip-pattern`]: cli.md#--test-skip-pattern
[`--test-update-snapshots`]: cli.md#--test-update-snapshots
[`--test`]: cli.md#--test
[`MockFileSystem`]: #class-mockfilesystem
[`MockFunctionContext`]: #class-mockfunctioncontext
[`MockPropertyContext`]: #class-mockpropertycontext
[`MockTimers`]: #class-mocktimers
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ Use this flag to enable ShadowRealm support.
.It Fl -experimental-test-coverage
Enable code coverage in the test runner.
.
.It Fl -experimental-test-fs-mocks
Enable file system mocking in the test runner.
.
.It Fl -experimental-test-module-mocks
Enable module mocking in the test runner.
.
Expand Down
Loading