Skip to content

Commit c12a9b7

Browse files
committed
doc: document mock file system for test runner
1 parent 581475d commit c12a9b7

File tree

2 files changed

+353
-0
lines changed

2 files changed

+353
-0
lines changed

doc/api/cli.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,23 @@ Enable module mocking in the test runner.
12941294

12951295
This feature requires `--allow-worker` if used with the [Permission Model][].
12961296

1297+
### `--experimental-test-fs-mocks`
1298+
1299+
<!-- YAML
1300+
added: REPLACEME
1301+
-->
1302+
1303+
> Stability: 1.0 - Early development
1304+
1305+
Enable file system mocking in the test runner.
1306+
1307+
This feature allows tests to mock file system operations without actually
1308+
reading from or writing to the disk. By default, virtual files take precedence
1309+
but real file system operations are still allowed for paths not defined in
1310+
the virtual file system. Use the `isolate: true` option to completely isolate
1311+
tests from the real file system. See the documentation on
1312+
[mocking the file system][] for more details.
1313+
12971314
### `--experimental-transform-types`
12981315

12991316
<!-- YAML
@@ -4234,6 +4251,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
42344251
[global setup and teardown]: test.md#global-setup-and-teardown
42354252
[jitless]: https://v8.dev/blog/jitless
42364253
[libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html
4254+
[mocking the file system]: test.md#file-system
42374255
[module compile cache]: module.md#module-compile-cache
42384256
[preloading asynchronous module customization hooks]: module.md#registration-of-asynchronous-customization-hooks
42394257
[remote code execution]: https://www.owasp.org/index.php/Code_Injection

doc/api/test.md

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,156 @@ test('runs timers as setTime passes ticks', (context) => {
10441044
});
10451045
```
10461046

1047+
### File system
1048+
1049+
<!-- YAML
1050+
added: REPLACEME
1051+
-->
1052+
1053+
> Stability: 1.0 - Early development
1054+
1055+
Mocking the file system is a technique commonly used in software testing to
1056+
simulate file operations without actually writing to or reading from the disk.
1057+
This allows for safer, faster, and more predictable tests when working with
1058+
file system operations.
1059+
1060+
Refer to the [`MockFileSystem`][] class for a full list of methods and features.
1061+
1062+
**Note:** This feature requires the `--experimental-test-fs-mocks` flag.
1063+
1064+
The example below shows how to mock file system operations. Using
1065+
`.enable({ files: {...} })` it will mock the file system methods in the
1066+
[node:fs](./fs.md) and [node:fs/promises](./fs.md#promises-api) modules.
1067+
1068+
```mjs
1069+
import assert from 'node:assert';
1070+
import fs from 'node:fs';
1071+
import { test } from 'node:test';
1072+
1073+
test('mocks file system operations', (context) => {
1074+
// Enable file system mocking with virtual files
1075+
context.mock.fs.enable({
1076+
files: {
1077+
'/virtual/test.txt': 'Hello, World!',
1078+
'/virtual/data.json': '{"key": "value"}',
1079+
},
1080+
});
1081+
1082+
// Read virtual files
1083+
const content = fs.readFileSync('/virtual/test.txt', 'utf8');
1084+
assert.strictEqual(content, 'Hello, World!');
1085+
1086+
// Write to virtual file system
1087+
fs.writeFileSync('/virtual/new.txt', 'New content');
1088+
assert.strictEqual(fs.readFileSync('/virtual/new.txt', 'utf8'), 'New content');
1089+
1090+
// Check if virtual file exists
1091+
assert.strictEqual(fs.existsSync('/virtual/test.txt'), true);
1092+
});
1093+
```
1094+
1095+
```cjs
1096+
const assert = require('node:assert');
1097+
const fs = require('node:fs');
1098+
const { test } = require('node:test');
1099+
1100+
test('mocks file system operations', (context) => {
1101+
// Enable file system mocking with virtual files
1102+
context.mock.fs.enable({
1103+
files: {
1104+
'/virtual/test.txt': 'Hello, World!',
1105+
'/virtual/data.json': '{"key": "value"}',
1106+
},
1107+
});
1108+
1109+
// Read virtual files
1110+
const content = fs.readFileSync('/virtual/test.txt', 'utf8');
1111+
assert.strictEqual(content, 'Hello, World!');
1112+
1113+
// Write to virtual file system
1114+
fs.writeFileSync('/virtual/new.txt', 'New content');
1115+
assert.strictEqual(fs.readFileSync('/virtual/new.txt', 'utf8'), 'New content');
1116+
1117+
// Check if virtual file exists
1118+
assert.strictEqual(fs.existsSync('/virtual/test.txt'), true);
1119+
});
1120+
```
1121+
1122+
By default, the mock file system allows access to both virtual and real files,
1123+
with virtual files taking precedence. When file system mocking is enabled,
1124+
**all write operations go to the virtual file system**, regardless of whether
1125+
the path exists in the real file system. This prevents tests from accidentally
1126+
modifying the real file system.
1127+
1128+
You can enable isolation mode to completely isolate tests from the real file
1129+
system for read operations as well:
1130+
1131+
```mjs
1132+
import assert from 'node:assert';
1133+
import fs from 'node:fs';
1134+
import { test } from 'node:test';
1135+
1136+
test('complete file system isolation', (context) => {
1137+
context.mock.fs.enable({
1138+
files: {
1139+
'/virtual/only.txt': 'Only this file exists',
1140+
},
1141+
isolate: true, // Enable full isolation mode
1142+
});
1143+
1144+
// Virtual file works
1145+
assert.strictEqual(fs.readFileSync('/virtual/only.txt', 'utf8'), 'Only this file exists');
1146+
1147+
// Real files are not accessible
1148+
assert.throws(() => {
1149+
fs.readFileSync('/etc/passwd');
1150+
}, {
1151+
code: 'ENOENT',
1152+
});
1153+
});
1154+
```
1155+
1156+
```cjs
1157+
const assert = require('node:assert');
1158+
const fs = require('node:fs');
1159+
const { test } = require('node:test');
1160+
1161+
test('complete file system isolation', (context) => {
1162+
context.mock.fs.enable({
1163+
files: {
1164+
'/virtual/only.txt': 'Only this file exists',
1165+
},
1166+
isolate: true, // Enable full isolation mode
1167+
});
1168+
1169+
// Virtual file works
1170+
assert.strictEqual(fs.readFileSync('/virtual/only.txt', 'utf8'), 'Only this file exists');
1171+
1172+
// Real files are not accessible
1173+
assert.throws(() => {
1174+
fs.readFileSync('/etc/passwd');
1175+
}, {
1176+
code: 'ENOENT',
1177+
});
1178+
});
1179+
```
1180+
1181+
#### Windows path handling
1182+
1183+
On Windows, use appropriate path separators or forward slashes:
1184+
1185+
```js
1186+
test('Windows path handling', (t) => {
1187+
t.mock.fs.enable({
1188+
files: {
1189+
'C:/virtual/test.txt': 'Hello, World!',
1190+
// or using backslashes
1191+
// 'C:\\virtual\\test.txt': 'Hello, World!',
1192+
},
1193+
});
1194+
});
1195+
```
1196+
10471197
## Snapshot testing
10481198

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

3259+
## Class: `MockFileSystem`
3260+
3261+
<!-- YAML
3262+
added: REPLACEME
3263+
-->
3264+
3265+
> Stability: 1.0 - Early development
3266+
3267+
Mocking the file system allows tests to simulate file operations without
3268+
actually reading from or writing to the disk. This makes tests safer, faster,
3269+
and more predictable.
3270+
3271+
The [`MockTracker`][] provides a top-level `fs` export
3272+
which is a `MockFileSystem` instance.
3273+
3274+
**Note:** This class requires the `--experimental-test-fs-mocks` flag.
3275+
3276+
### `fs.enable([options])`
3277+
3278+
<!-- YAML
3279+
added: REPLACEME
3280+
-->
3281+
3282+
Enables file system mocking.
3283+
3284+
* `options` {Object} Optional configuration options for enabling file system
3285+
mocking. The following properties are supported:
3286+
* `files` {Object} An object mapping file paths to their content. Content
3287+
can be a string, `Buffer`, or `Uint8Array`. Strings are automatically
3288+
converted to `Buffer` using UTF-8 encoding. **Default:** `{}`.
3289+
* `isolate` {boolean} If `true`, only virtual files are accessible and
3290+
any access to paths not in `files` will throw `ENOENT`. If `false`
3291+
(the default), virtual files take precedence but real file system
3292+
operations are still allowed for other paths. **Note:** When mocking is
3293+
enabled, write operations always go to the virtual file system regardless
3294+
of this setting. **Default:** `false`.
3295+
* `apis` {Array} An optional array specifying which fs API families to mock.
3296+
Each value mocks the synchronous, callback, and promise versions of that
3297+
API (e.g., `'readFile'` mocks `fs.readFileSync()`, `fs.readFile()`, and
3298+
`fsPromises.readFile()`). The supported values are `'readFile'`,
3299+
`'writeFile'`, `'appendFile'`, `'stat'`, `'lstat'`, `'access'`, `'exists'`,
3300+
`'unlink'`, `'mkdir'`, `'rmdir'`, and `'readdir'`. **Default:** all
3301+
supported APIs.
3302+
3303+
**Note:** When file system mocking is enabled, the mock automatically
3304+
creates parent directories for all virtual files.
3305+
3306+
Example usage:
3307+
3308+
```mjs
3309+
import { mock } from 'node:test';
3310+
import { Buffer } from 'node:buffer';
3311+
3312+
mock.fs.enable({
3313+
files: {
3314+
'/path/to/file.txt': 'file content',
3315+
'/path/to/binary.bin': Buffer.from([0x00, 0x01, 0x02]),
3316+
},
3317+
});
3318+
```
3319+
3320+
```cjs
3321+
const { mock } = require('node:test');
3322+
3323+
mock.fs.enable({
3324+
files: {
3325+
'/path/to/file.txt': 'file content',
3326+
'/path/to/binary.bin': Buffer.from([0x00, 0x01, 0x02]),
3327+
},
3328+
});
3329+
```
3330+
3331+
### `fs.reset()`
3332+
3333+
<!-- YAML
3334+
added: REPLACEME
3335+
-->
3336+
3337+
Restores the original file system functions and clears all virtual files.
3338+
This function is automatically called when a test using the mock file system
3339+
completes.
3340+
3341+
### Supported `fs` methods
3342+
3343+
The following methods are intercepted by the mock file system:
3344+
3345+
**Synchronous methods:**
3346+
3347+
* `fs.readFileSync()`
3348+
* `fs.writeFileSync()`
3349+
* `fs.appendFileSync()`
3350+
* `fs.statSync()`
3351+
* `fs.lstatSync()`
3352+
* `fs.existsSync()`
3353+
* `fs.accessSync()`
3354+
* `fs.unlinkSync()`
3355+
* `fs.mkdirSync()`
3356+
* `fs.rmdirSync()`
3357+
* `fs.readdirSync()`
3358+
3359+
**Callback methods:**
3360+
3361+
* `fs.readFile()`
3362+
* `fs.writeFile()`
3363+
* `fs.appendFile()`
3364+
* `fs.stat()`
3365+
* `fs.lstat()`
3366+
* `fs.exists()`
3367+
* `fs.access()`
3368+
* `fs.unlink()`
3369+
* `fs.mkdir()`
3370+
* `fs.rmdir()`
3371+
* `fs.readdir()`
3372+
3373+
**Promise methods (`fs/promises`):**
3374+
3375+
* `fsPromises.readFile()`
3376+
* `fsPromises.writeFile()`
3377+
* `fsPromises.appendFile()`
3378+
* `fsPromises.stat()`
3379+
* `fsPromises.lstat()`
3380+
* `fsPromises.access()`
3381+
* `fsPromises.unlink()`
3382+
* `fsPromises.mkdir()`
3383+
* `fsPromises.rmdir()`
3384+
* `fsPromises.readdir()`
3385+
3386+
### Limitations
3387+
3388+
The mock file system has the following limitations:
3389+
3390+
* **Symbolic links are not supported.** `lstat()` behaves identically to
3391+
`stat()`, and `isSymbolicLink()` always returns `false`.
3392+
* **Dirent objects are not `fs.Dirent` instances.** The objects returned by
3393+
`readdir({ withFileTypes: true })` have the same properties and methods as
3394+
`fs.Dirent`, but `dirent instanceof fs.Dirent` will return `false`.
3395+
* **The following methods are not mocked:**
3396+
* `fs.copyFile()` / `fs.copyFileSync()`
3397+
* `fs.rename()` / `fs.renameSync()`
3398+
* `fs.chmod()` / `fs.chmodSync()` / `fs.chown()` / `fs.chownSync()`
3399+
* `fs.realpath()` / `fs.realpathSync()`
3400+
* `fs.watch()` / `fs.watchFile()` / `fs.unwatchFile()`
3401+
* `fs.open()` / `fs.openSync()` and file descriptor operations
3402+
* `fs.createReadStream()` / `fs.createWriteStream()`
3403+
* **File permissions are not enforced.** All virtual files are created with
3404+
mode `0o644` and permission checks are not performed.
3405+
* **File descriptors are not supported.** Operations that require file
3406+
descriptors will not work with virtual files.
3407+
* **`mkdir()` return value.** When called with `{ recursive: true }`,
3408+
`mkdir()` returns the first directory path created (matching real `fs`
3409+
behavior). Without `recursive`, it returns `undefined`.
3410+
* **Recursive `readdir()` is not supported.** Calling `readdir()` with
3411+
`{ recursive: true }` will throw `ERR_INVALID_ARG_VALUE`.
3412+
3413+
### Stats object
3414+
3415+
Mock stats objects have the following properties with default values:
3416+
3417+
| Property | Type | Default |
3418+
| ------------------------------------------- | -------- | -------------------------------- |
3419+
| `dev` | `number` | `0` |
3420+
| `ino` | `number` | `0` |
3421+
| `mode` | `number` | File: `0o100644`, Dir: `0o40644` |
3422+
| `nlink` | `number` | `1` |
3423+
| `uid` | `number` | `0` |
3424+
| `gid` | `number` | `0` |
3425+
| `rdev` | `number` | `0` |
3426+
| `size` | `number` | Content length |
3427+
| `blksize` | `number` | `4096` |
3428+
| `blocks` | `number` | `ceil(size / 512)` |
3429+
| `atime`/`mtime`/`ctime`/`birthtime` | `Date` | Creation time |
3430+
| `atimeMs`/`mtimeMs`/`ctimeMs`/`birthtimeMs` | `number` | Creation time (ms) |
3431+
3432+
The stats object includes the following methods that return the expected
3433+
values for virtual files and directories:
3434+
3435+
* `isFile()`
3436+
* `isDirectory()`
3437+
* `isBlockDevice()` - always returns `false`
3438+
* `isCharacterDevice()` - always returns `false`
3439+
* `isSymbolicLink()` - always returns `false`
3440+
* `isFIFO()` - always returns `false`
3441+
* `isSocket()` - always returns `false`
3442+
31093443
## Class: `TestsStream`
31103444

31113445
<!-- YAML
@@ -4056,6 +4390,7 @@ Can be used to abort test subtasks when the test has been aborted.
40564390
[`--test-skip-pattern`]: cli.md#--test-skip-pattern
40574391
[`--test-update-snapshots`]: cli.md#--test-update-snapshots
40584392
[`--test`]: cli.md#--test
4393+
[`MockFileSystem`]: #class-mockfilesystem
40594394
[`MockFunctionContext`]: #class-mockfunctioncontext
40604395
[`MockPropertyContext`]: #class-mockpropertycontext
40614396
[`MockTimers`]: #class-mocktimers

0 commit comments

Comments
 (0)