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
145 changes: 145 additions & 0 deletions doc/api/test.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2331,6 +2331,89 @@ This function is used to customize the location of the snapshot file used for
snapshot testing. By default, the snapshot filename is the same as the entry
point filename with a `.snapshot` file extension.

## Class: `MockFSContext`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

The `MockFSContext` class is returned by [`mock.fs()`][] and is used to
inspect and extend a mock file system.

### `mockFs.addDirectory(path)`

<!-- YAML
added: REPLACEME
-->

* `path` {string} The path of the directory, relative to the mount point.
* Returns: {string} The absolute path of the created directory.

Adds a directory to the mock file system. Missing parent directories are
created automatically.

### `mockFs.addFile(path, content)`

<!-- YAML
added: REPLACEME
-->

* `path` {string} The path of the file, relative to the mount point.
* `content` {string|Buffer|TypedArray|DataView} The file content.
* Returns: {string} The absolute path of the created file.

Adds a file to the mock file system. Missing parent directories are
created automatically.

### `mockFs.existsSync(path)`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand why we'd want/need these separate APIs (like existsSync/addRirectory etc) the user can just call mockFs.vfs.mkdirSync can't they?


<!-- YAML
added: REPLACEME
-->

* `path` {string} The path to check, relative to the mount point.
* Returns: {boolean}

Returns `true` if the path exists in the mock file system, and `false`
otherwise, including once the mock has been restored.

### `mockFs.mountPoint`

<!-- YAML
added: REPLACEME
-->

* Type: {string|null}

The absolute path where the mock file system is mounted, or `null` once
the mock has been restored. The mount point is a reserved path assigned
by the [virtual file system][] when the mock is created; it never
shadows real files or directories. Join it with relative paths to access
the mock's files through the `node:fs` APIs.

### `mockFs.restore()`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For restore to make sense here IMO the vfs would need to actually "take over" something like other mocks. I don't understand when I'd ever do this.

OTOH if mockFs could intercept/take over calls (i.e. like mock.module) it would make sense.

This is cool but I don't understand why I'd use this over mock.module("node:fs", { exports: new VirtualFileSystem() })


<!-- YAML
added: REPLACEME
-->

Unmounts the mock file system. Once restored, the mock's files are no
longer accessible and files can no longer be added. Calling this
function more than once has no effect. This function is called
automatically when the associated test finishes.

### `mockFs.vfs`

<!-- YAML
added: REPLACEME
-->

* Type: {VirtualFileSystem}

The underlying [`VirtualFileSystem`][] instance.

## Class: `MockFunctionContext`

<!-- YAML
Expand DownExpand Up@@ -2658,6 +2741,64 @@ test('mocks a counting function', (t) => {
});
```

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

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

* `options` {Object} Optional configuration options for the mock file
system. The following properties are supported:
* `files` {Object} Initial files to create. Keys are file paths relative
to the mount point, and values are the file contents as {string} or
{Buffer}. Missing parent directories are created automatically.
* Returns: {MockFSContext} An object that can be used to manage the mock
file system.

This function creates an in-memory mock file system backed by the
[virtual file system][]. The mock is mounted at a reserved mount point
that is assigned when the mock is created and exposed as
[`mockFs.mountPoint`][], so it never shadows real files or directories.
Paths obtained by joining the mount point with a relative path work with
the regular `node:fs` APIs and can be loaded with `require()` and
`import`.

If this function is invoked through a `TestContext`, the mock file
system is unmounted automatically when the test finishes.

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

test('reads configuration from a mock file', (t) => {
const mockFs = t.mock.fs({
files: {
'config.json': JSON.stringify({ debug: true }),
'data/users.txt': 'user1\nuser2\nuser3',
},
});

// Files are accessible via standard fs APIs under the mount point.
const configPath = path.join(mockFs.mountPoint, 'config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.strictEqual(config.debug, true);

// Files can be added after creation. addFile() returns the
// absolute path of the new file.
const readmePath = mockFs.addFile('README.md', '# Hello');
assert.strictEqual(fs.readFileSync(readmePath, 'utf8'), '# Hello');

// Modules in the mock file system can be loaded with require()
// and import.
const modPath = mockFs.addFile('mod.js', 'module.exports = 42;');
assert.strictEqual(require(modPath), 42);
});
```

### `mock.getter(object, methodName[, implementation][, options])`

<!-- YAML
Expand DownExpand Up@@ -5035,6 +5176,7 @@ test.describe('my suite', (suite) => {
[`SuiteContext`]: #class-suitecontext
[`TestContext`]: #class-testcontext
[`TracingChannel`]: diagnostics_channel.md#class-tracingchannel
[`VirtualFileSystem`]: vfs.md#class-virtualfilesystem
[`assert.throws`]: assert.md#assertthrowsfn-error-message
[`context.diagnostic`]: #contextdiagnosticmessage
[`context.log`]: #contextlogmessage-data
Expand All@@ -5045,6 +5187,8 @@ test.describe('my suite', (suite) => {
[`diagnostics_channel`]: diagnostics_channel.md
[`glob(7)`]: https://man7.org/linux/man-pages/man7/glob.7.html
[`it()`]: #itname-options-fn
[`mock.fs()`]: #mockfsoptions
[`mockFs.mountPoint`]: #mockfsmountpoint
[`run()`]: #runoptions
[`suite()`]: #suitename-options-fn
[`test()`]: #testname-options-fn
Expand All@@ -5059,3 +5203,4 @@ test.describe('my suite', (suite) => {
[suite options]: #suitename-options-fn
[test reporters]: #test-reporters
[test runner execution model]: #test-runner-execution-model
[virtual file system]: vfs.md
7 changes: 7 additions & 0 deletions doc/api/vfs.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -450,6 +450,12 @@ The SEA configuration parser will error if either combination is detected.
See the [Single Executable Application][] documentation for more information
on creating SEA builds with assets.

## Use with the test runner

The [`mock.fs()`][] API of the `node:test` module creates a mock file
system backed by a mounted `VirtualFileSystem`, which is unmounted
automatically when the associated test finishes.

## Class: `VirtualProvider`

<!-- YAML
Expand DownExpand Up@@ -631,6 +637,7 @@ fields use synthetic but stable values:
[`fs.BigIntStats`]: fs.md#class-fsstats
[`fs.Stats`]: fs.md#class-fsstats
[`import.meta.resolve()`]: esm.md#importmetaresolvespecifier
[`mock.fs()`]: test.md#mockfsoptions
[`node:fs`]: fs.md
[`require()`]: modules.md#requireid
[`require.resolve()`]: modules.md#requireresolverequest-options
Expand Down
139 changes: 139 additions & 0 deletions lib/internal/test_runner/mock/mock.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,8 +54,11 @@ const {
validateInteger,
validateObject,
validateOneOf,
validateString,
} = require('internal/validators');
const { MockTimers } = require('internal/test_runner/mock/mock_timers');
const { isArrayBufferView } = require('internal/util/types');
const { dirname, join } = require('path');
const {
Module,
} = require('internal/modules/cjs/loader');
Expand DownExpand Up@@ -449,6 +452,103 @@ class MockPropertyContext {

const { restore: restoreProperty } = MockPropertyContext.prototype;

/**
* Context for a mock file system backed by a mounted virtual file
* system. Returned by MockTracker.fs().
*/
class MockFSContext {
#vfs;

constructor(vfs) {
this.#vfs = vfs;
}

/**
* The underlying VirtualFileSystem instance.
* @type {VirtualFileSystem}
*/
get vfs() {
return this.#vfs;
}

/**
* The mount point of the mock file system, or null once restored.
* @type {string|null}
*/
get mountPoint() {
return this.#vfs.mountPoint;
}

#resolve(filePath) {
validateString(filePath, 'path');
const mountPoint = this.#vfs.mountPoint;
if (mountPoint === null) {
throw new ERR_INVALID_STATE('The mock file system has been restored');
}
return join(mountPoint, filePath);
}

/**
* Adds a file to the mock file system, creating parent directories
* as needed.
* @param {string} filePath - The path of the file, relative to the mount
* point.
* @param {string|Buffer} content - The file content.
* @returns {string} The absolute path of the created file.
*/
addFile(filePath, content) {
const fullPath = this.#resolve(filePath);
if (typeof content !== 'string' && !isArrayBufferView(content)) {
throw new ERR_INVALID_ARG_TYPE(
'content', ['string', 'Buffer', 'TypedArray', 'DataView'], content,
);
}
const parentDir = dirname(fullPath);
if (parentDir !== this.#vfs.mountPoint) {
this.#vfs.mkdirSync(parentDir, { __proto__: null, recursive: true });
}
this.#vfs.writeFileSync(fullPath, content);
return fullPath;
}

/**
* Adds a directory to the mock file system, creating parent
* directories as needed.
* @param {string} dirPath - The path of the directory, relative to the
* mount point.
* @returns {string} The absolute path of the created directory.
*/
addDirectory(dirPath) {
const fullPath = this.#resolve(dirPath);
this.#vfs.mkdirSync(fullPath, { __proto__: null, recursive: true });
return fullPath;
}

/**
* Checks if a path exists in the mock file system.
* @param {string} filePath - The path to check, relative to the mount
* point.
* @returns {boolean}
*/
existsSync(filePath) {
if (this.#vfs.mountPoint === null) {
return false;
}
return this.#vfs.existsSync(this.#resolve(filePath));
}

/**
* Unmounts the mock file system.
*/
restore() {
if (this.#vfs.mounted) {
this.#vfs.unmount();
}
}
}

const { restore: restoreFileSystem } = MockFSContext.prototype;

class MockTracker {
#mocks = [];
#timers;
Expand DownExpand Up@@ -493,6 +593,45 @@ class MockTracker {
return this.#setupMock(ctx, original);
}

/**
* Creates a mock file system backed by a mounted virtual file system.
* @param {object} [options] - Options for the mock file system.
* @param {object} [options.files] - Initial files to create. Keys are file
* paths relative to the mount point and values are the file contents.
* @returns {MockFSContext} The mock file system context.
*/
fs(options = kEmptyObject) {
emitExperimentalWarning('The mock.fs API');
validateObject(options, 'options');
const { files } = options;
if (files !== undefined) {
validateObject(files, 'options.files');
}

const { VirtualFileSystem } = require('internal/vfs/file_system');
const vfs = new VirtualFileSystem({
__proto__: null,
emitExperimentalWarning: false,
});
vfs.mount();
const ctx = new MockFSContext(vfs);

if (files !== undefined) {
const paths = ObjectKeys(files);
for (let i = 0; i < paths.length; i++) {
ctx.addFile(paths[i], files[paths[i]]);
}
}

ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreFileSystem,
});

return ctx;
}

/**
* Creates a method tracker for a specified object or function.
* @param {(object | Function)} objectOrFunction - The object or function containing the method to be tracked.
Expand Down
Loading
Loading