测试运行器#

稳定性:2 - 稳定

源代码: lib/test.js

node:test 模块便于创建 JavaScript 测试。要访问它

import test from 'node:test';const test = require('node:test');

此模块仅在 node: 方案下可用。

通过 test 模块创建的测试由单个函数组成,该函数以以下三种方式之一处理

  1. 一个同步函数,如果它抛出异常,则被认为是失败的,否则被认为是通过的。
  2. 一个返回 Promise 的函数,如果 Promise 拒绝,则被认为是失败的,如果 Promise 兑现,则被认为是通过的。
  3. 一个接收回调函数的函数。如果回调接收到任何真值作为其第一个参数,则该测试被认为是失败的。如果将假值作为第一个参数传递给回调,则该测试被认为是通过的。如果测试函数接收到回调函数并且还返回一个 Promise,则测试将失败。

以下示例说明了如何使用 test 模块编写测试。

test('synchronous passing test', (t) => {
  // This test passes because it does not throw an exception.
  assert.strictEqual(1, 1);
});

test('synchronous failing test', (t) => {
  // This test fails because it throws an exception.
  assert.strictEqual(1, 2);
});

test('asynchronous passing test', async (t) => {
  // This test passes because the Promise returned by the async
  // function is settled and not rejected.
  assert.strictEqual(1, 1);
});

test('asynchronous failing test', async (t) => {
  // This test fails because the Promise returned by the async
  // function is rejected.
  assert.strictEqual(1, 2);
});

test('failing test using Promises', (t) => {
  // Promises can be used directly as well.
  return new Promise((resolve, reject) => {
    setImmediate(() => {
      reject(new Error('this will cause the test to fail'));
    });
  });
});

test('callback passing test', (t, done) => {
  // done() is the callback function. When the setImmediate() runs, it invokes
  // done() with no arguments.
  setImmediate(done);
});

test('callback failing test', (t, done) => {
  // When the setImmediate() runs, done() is invoked with an Error object and
  // the test fails.
  setImmediate(() => {
    done(new Error('callback failure'));
  });
}); 

如果任何测试失败,进程退出代码将设置为 1

子测试#

测试上下文的 test() 方法允许创建子测试。它允许您以分层方式构建测试,您可以在更大的测试中创建嵌套测试。此方法的行为与顶级 test() 函数完全相同。以下示例演示了创建具有两个子测试的顶级测试。

test('top level test', async (t) => {
  t.test('subtest 1', (t) => {
    assert.strictEqual(1, 1);
  });

  t.test('subtest 2', (t) => {
    assert.strictEqual(2, 2);
  });
}); 

注意: beforeEachafterEach 钩子在每个子测试执行之间触发。

任何子测试失败都会导致父测试失败。

跳过测试#

可以通过将 skip 选项传递给测试来跳过单个测试,或者通过调用测试上下文的 skip() 方法来跳过单个测试,如以下示例所示。

// The skip option is used, but no message is provided.
test('skip option', { skip: true }, (t) => {
  // This code is never executed.
});

// The skip option is used, and a message is provided.
test('skip option with message', { skip: 'this is skipped' }, (t) => {
  // This code is never executed.
});

test('skip() method', (t) => {
  // Make sure to return here as well if the test contains additional logic.
  t.skip();
});

test('skip() method with message', (t) => {
  // Make sure to return here as well if the test contains additional logic.
  t.skip('this is skipped');
}); 

TODO 测试#

可以通过将 todo 选项传递给测试,或通过调用测试上下文的 todo() 方法将单个测试标记为不稳定的或不完整的,如以下示例所示。这些测试代表需要修复的待处理实现或错误。TODO 测试会被执行,但不被视为测试失败,因此不会影响进程退出代码。如果一个测试被标记为 TODO 和跳过,则忽略 TODO 选项。

// The todo option is used, but no message is provided.
test('todo option', { todo: true }, (t) => {
  // This code is executed, but not treated as a failure.
  throw new Error('this does not fail the test');
});

// The todo option is used, and a message is provided.
test('todo option with message', { todo: 'this is a todo test' }, (t) => {
  // This code is executed.
});

test('todo() method', (t) => {
  t.todo();
});

test('todo() method with message', (t) => {
  t.todo('this is a todo test and is not treated as a failure');
  throw new Error('this does not fail the test');
}); 

describe()it() 别名#

也可以使用 describe()it() 函数编写套件和测试。describe()suite() 的别名,it()test() 的别名。

describe('A thing', () => {
  it('should work', () => {
    assert.strictEqual(1, 1);
  });

  it('should be ok', () => {
    assert.strictEqual(2, 2);
  });

  describe('a nested thing', () => {
    it('should work', () => {
      assert.strictEqual(3, 3);
    });
  });
}); 

describe()it()node:test 模块导入。

import { describe, it } from 'node:test';const { describe, it } = require('node:test');

only 测试#

如果 Node.js 使用 --test-only 命令行选项启动,或者测试隔离被禁用,则可以通过将 only 选项传递给应该运行的测试来跳过除所选子集之外的所有测试。当设置了 only 选项的测试时,所有子测试也会运行。如果套件设置了 only 选项,则将运行套件中的所有测试,除非它有设置了 only 选项的后代,在这种情况下,仅运行这些测试。

当在 test()/it() 中使用 子测试时,需要使用 only 选项标记所有祖先测试,以便仅运行所选的测试子集。

测试上下文的 runOnly() 方法可用于在子测试级别实现相同的行为。未执行的测试将从测试运行器输出中省略。

// Assume Node.js is run with the --test-only command-line option.
// The suite's 'only' option is set, so these tests are run.
test('this test is run', { only: true }, async (t) => {
  // Within this test, all subtests are run by default.
  t.test('running subtest');

  // The test context can be updated to run subtests with the 'only' option.
  t.runOnly(true);
  t.test('this subtest is now skipped');
  t.test('this subtest is run', { only: true });

  // Switch the context back to execute all tests.
  t.runOnly(false);
  t.test('this subtest is now run');

  // Explicitly do not run these tests.
  t.test('skipped subtest 3', { only: false });
  t.test('skipped subtest 4', { skip: true });
});

// The 'only' option is not set, so this test is skipped.
test('this test is not run', () => {
  // This code is not run.
  throw new Error('fail');
});

describe('a suite', () => {
  // The 'only' option is set, so this test is run.
  it('this test is run', { only: true }, () => {
    // This code is run.
  });

  it('this test is not run', () => {
    // This code is not run.
    throw new Error('fail');
  });
});

describe.only('a suite', () => {
  // The 'only' option is set, so this test is run.
  it('this test is run', () => {
    // This code is run.
  });

  it('this test is run', () => {
    // This code is run.
  });
}); 

按名称过滤测试#

--test-name-pattern 命令行选项可用于仅运行名称与提供的模式匹配的测试,并且可以使用 --test-skip-pattern 选项来跳过名称与提供的模式匹配的测试。测试名称模式被解释为 JavaScript 正则表达式。可以多次指定 --test-name-pattern--test-skip-pattern 选项,以便运行嵌套测试。对于执行的每个测试,还将运行任何相应的测试钩子,例如 beforeEach()。未执行的测试将从测试运行器输出中省略。

给定以下测试文件,使用 --test-name-pattern="test [1-3]" 选项启动 Node.js 将导致测试运行器执行 test 1test 2test 3。如果 test 1 与测试名称模式不匹配,那么它的子测试将不会执行,尽管它们匹配该模式。也可以通过多次传递 --test-name-pattern 来执行同一组测试(例如,--test-name-pattern="test 1"--test-name-pattern="test 2" 等)。

test('test 1', async (t) => {
  t.test('test 2');
  t.test('test 3');
});

test('Test 4', async (t) => {
  t.test('Test 5');
  t.test('test 6');
}); 

测试名称模式也可以使用正则表达式字面量来指定。这允许使用正则表达式标志。在前面的例子中,使用 --test-name-pattern="/test [4-5]/i" (或 --test-skip-pattern="/test [4-5]/i") 启动 Node.js 将匹配 Test 4Test 5,因为模式是大小写不敏感的。

要使用模式匹配单个测试,你可以使用空格分隔所有其父级测试名称作为前缀,以确保它的唯一性。例如,给定以下测试文件

describe('test 1', (t) => {
  it('some test');
});

describe('test 2', (t) => {
  it('some test');
}); 

使用 --test-name-pattern="test 1 some test" 启动 Node.js 将只匹配 test 1 中的 some test

测试名称模式不会更改测试运行程序执行的文件集。

如果同时提供了 --test-name-pattern--test-skip-pattern,则测试必须满足**两者**要求才能被执行。

多余的异步活动#

一旦测试函数执行完成,结果会尽可能快地报告,同时保持测试的顺序。但是,测试函数有可能生成超出测试本身的异步活动。测试运行程序会处理此类活动,但不会为了适应它而延迟测试结果的报告。

在下面的例子中,一个测试完成时还有两个未完成的 setImmediate() 操作。第一个 setImmediate() 尝试创建一个新的子测试。由于父测试已经完成并输出了它的结果,新的子测试会立即被标记为失败,并稍后报告给 <TestsStream>

第二个 setImmediate() 创建一个 uncaughtException 事件。源自已完成测试的 uncaughtExceptionunhandledRejection 事件会被 test 模块标记为失败,并由 <TestsStream> 在顶层报告为诊断警告。

test('a test that creates asynchronous activity', (t) => {
  setImmediate(() => {
    t.test('subtest that is created too late', (t) => {
      throw new Error('error1');
    });
  });

  setImmediate(() => {
    throw new Error('error2');
  });

  // The test finishes after this line.
}); 

监听模式#

稳定性: 1 - 实验性

Node.js 测试运行器支持通过传递 --watch 标志在监听模式下运行

node --test --watch 

在监听模式下,测试运行器会监听测试文件及其依赖项的更改。当检测到更改时,测试运行器会重新运行受更改影响的测试。测试运行器将持续运行,直到进程终止。

全局设置和拆卸#

稳定性: 1.0 - 早期开发

测试运行器支持指定一个模块,该模块将在所有测试执行之前被评估,并可用于设置全局状态或测试的fixture。这对于准备资源或设置多个测试所需的共享状态非常有用。

此模块可以导出以下任何内容

  • 一个 globalSetup 函数,它在所有测试开始前运行一次
  • 一个 globalTeardown 函数,它在所有测试完成后运行一次

使用命令行运行测试时,使用 --test-global-setup 标志指定模块。

// setup-module.js
async function globalSetup() {
  // Setup shared resources, state, or environment
  console.log('Global setup executed');
  // Run servers, create files, prepare databases, etc.
}

async function globalTeardown() {
  // Clean up resources, state, or environment
  console.log('Global teardown executed');
  // Close servers, remove files, disconnect from databases, etc.
}

module.exports = { globalSetup, globalTeardown };// setup-module.mjs
export async function globalSetup() {
  // Setup shared resources, state, or environment
  console.log('Global setup executed');
  // Run servers, create files, prepare databases, etc.
}

export async function globalTeardown() {
  // Clean up resources, state, or environment
  console.log('Global teardown executed');
  // Close servers, remove files, disconnect from databases, etc.
}

如果全局 setup 函数抛出一个错误,将不会运行任何测试,并且进程将以非零退出码退出。 在这种情况下,将不会调用全局 teardown 函数。

从命令行运行测试#

可以通过传递 --test 标志从命令行调用 Node.js 测试运行器

node --test 

默认情况下,Node.js 将运行所有匹配以下模式的文件

  • **/*.test.{cjs,mjs,js}
  • **/*-test.{cjs,mjs,js}
  • **/*_test.{cjs,mjs,js}
  • **/test-*.{cjs,mjs,js}
  • **/test.{cjs,mjs,js}
  • **/test/**/*.{cjs,mjs,js}

除非提供了 --no-experimental-strip-types,否则还会匹配以下附加模式

  • **/test/**/*-test.{cts,mts,ts}
  • **/test/**/*.test.{cts,mts,ts}
  • **/test/**/*_test.{cts,mts,ts}

或者,可以将一个或多个 glob 模式作为 Node.js 命令的最后一个参数提供,如下所示。Glob 模式遵循 glob(7) 的行为。glob 模式应该用双引号括起来,以防止 shell 扩展,这可以降低跨系统的可移植性。

node --test "**/*.test.js" "**/*.spec.js" 

匹配的文件将作为测试文件执行。有关测试文件执行的更多信息可以在 测试运行器执行模型 部分找到。

测试运行器执行模型#

当启用进程级测试隔离时,每个匹配的测试文件都在一个单独的子进程中执行。 任何时候运行的子进程的最大数量由 --test-concurrency 标志控制。如果子进程以退出代码 0 结束,则认为测试通过。 否则,测试被认为是失败的。测试文件必须可由 Node.js 执行,但不需要在内部使用 node:test 模块。

每个测试文件都以普通脚本的方式执行。也就是说,如果测试文件本身使用 node:test 来定义测试,所有这些测试都将在单个应用程序线程中执行,而不管 test()concurrency 选项的值如何。

当禁用进程级测试隔离时,每个匹配的测试文件都会被导入到测试运行程序进程中。 一旦所有测试文件都已加载,顶级测试将以 1 的并发性执行。因为所有测试文件都在同一个上下文中运行,所以测试可能以在启用隔离时不可能的方式相互交互。例如,如果一个测试依赖于全局状态,那么该状态可能被来自另一个文件的测试修改。

收集代码覆盖率#

稳定性: 1 - 实验性

当使用 --experimental-test-coverage 命令行标志启动 Node.js 时,代码覆盖率会被收集,并且一旦所有测试完成,统计数据就会被报告。 如果 NODE_V8_COVERAGE 环境变量用于指定代码覆盖率目录,则生成的 V8 覆盖率文件会被写入该目录。默认情况下,Node.js 核心模块和 node_modules/ 目录中的文件不包含在覆盖率报告中。 但是,可以通过 --test-coverage-include 标志显式包含它们。默认情况下,所有匹配的测试文件都会从覆盖率报告中排除。 可以使用 --test-coverage-exclude 标志覆盖排除项。如果启用了覆盖率,覆盖率报告会通过 'test:coverage' 事件发送给任何 测试报告器

可以使用以下注释语法在一系列行上禁用覆盖率

/* node:coverage disable */
if (anAlwaysFalseCondition) {
  // Code in this branch will never be executed, but the lines are ignored for
  // coverage purposes. All lines following the 'disable' comment are ignored
  // until a corresponding 'enable' comment is encountered.
  console.log('this is never executed');
}
/* node:coverage enable */ 

也可以为指定数量的行禁用覆盖率。在指定的行数之后,覆盖率将自动重新启用。如果未明确提供行数,则忽略单行。

/* node:coverage ignore next */
if (anAlwaysFalseCondition) { console.log('this is never executed'); }

/* node:coverage ignore next 3 */
if (anAlwaysFalseCondition) {
  console.log('this is never executed');
} 

覆盖率报告器#

tap 和 spec 报告器将打印覆盖率统计信息的摘要。 还有一个 lcov 报告器,它将生成一个 lcov 文件,该文件可以用作深入的覆盖率报告。

node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info 
  • 此报告器不报告任何测试结果。
  • 理想情况下,此报告器应与另一个报告器一起使用。

模拟#

node:test 模块通过顶层 mock 对象支持测试期间的模拟。 以下示例在将两个数字加在一起的函数上创建一个 spy。 然后使用 spy 来断言该函数已按预期调用。

import assert from 'node:assert';
import { mock, test } from 'node:test';

test('spies on a function', () => {
  const sum = mock.fn((a, b) => {
    return a + b;
  });

  assert.strictEqual(sum.mock.callCount(), 0);
  assert.strictEqual(sum(3, 4), 7);
  assert.strictEqual(sum.mock.callCount(), 1);

  const call = sum.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.result, 7);
  assert.strictEqual(call.error, undefined);

  // Reset the globally tracked mocks.
  mock.reset();
});'use strict';
const assert = require('node:assert');
const { mock, test } = require('node:test');

test('spies on a function', () => {
  const sum = mock.fn((a, b) => {
    return a + b;
  });

  assert.strictEqual(sum.mock.callCount(), 0);
  assert.strictEqual(sum(3, 4), 7);
  assert.strictEqual(sum.mock.callCount(), 1);

  const call = sum.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.result, 7);
  assert.strictEqual(call.error, undefined);

  // Reset the globally tracked mocks.
  mock.reset();
});

相同的模拟功能也在每个测试的 TestContext 对象上公开。 以下示例使用 TestContext 上公开的 API 在对象方法上创建一个 spy。 通过测试上下文进行模拟的好处是,一旦测试完成,测试运行程序将自动恢复所有模拟的功能。

test('spies on an object method', (t) => {
  const number = {
    value: 5,
    add(a) {
      return this.value + a;
    },
  };

  t.mock.method(number, 'add');
  assert.strictEqual(number.add.mock.callCount(), 0);
  assert.strictEqual(number.add(3), 8);
  assert.strictEqual(number.add.mock.callCount(), 1);

  const call = number.add.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [3]);
  assert.strictEqual(call.result, 8);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, number);
}); 

计时器#

模拟计时器是软件测试中常用的一种技术,用于模拟和控制计时器的行为,例如 setIntervalsetTimeout,而无需实际等待指定的时间间隔。

有关方法和功能的完整列表,请参阅 MockTimers 类。

这允许开发人员为时间相关的函数编写更可靠和可预测的测试。

下面的示例显示了如何模拟 setTimeout。 使用 .enable({ apis: ['setTimeout'] }); 它将模拟 node:timersnode:timers/promises 模块中的 setTimeout 函数,以及来自 Node.js 全局上下文。

注意: 此 API 目前不支持解构函数,例如 import { setTimeout } from 'node:timers'

import assert from 'node:assert';
import { mock, test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', () => {
  const fn = mock.fn();

  // Optionally choose what to mock
  mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Advance in time
  mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);

  // Reset the globally tracked mocks.
  mock.timers.reset();

  // If you call reset mock instance, it will also reset timers instance
  mock.reset();
});const assert = require('node:assert');
const { mock, test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', () => {
  const fn = mock.fn();

  // Optionally choose what to mock
  mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Advance in time
  mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);

  // Reset the globally tracked mocks.
  mock.timers.reset();

  // If you call reset mock instance, it will also reset timers instance
  mock.reset();
});

相同的模拟功能也在每个测试的 TestContext 对象的 mock 属性中公开。 通过测试上下文进行模拟的好处是,一旦测试完成,测试运行程序将自动恢复所有模拟的计时器功能。

import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Advance in time
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
});const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Advance in time
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
});

日期#

mock timers API 还允许模拟 Date 对象。 这是测试时间相关的函数或模拟内部日历函数(如 Date.now())的有用功能。

dates 实现也是 MockTimers 类的一部分。 有关方法和功能的完整列表,请参阅它。

注意: 当一起模拟时,日期和计时器是相关的。 这意味着如果你同时模拟了 DatesetTimeout,推进时间也将推进模拟的日期,因为它们模拟的是单个内部时钟。

下面的例子展示了如何模拟 Date 对象并获取当前的 Date.now() 值。

import assert from 'node:assert';
import { test } from 'node:test';

test('mocks the Date object', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['Date'] });
  // If not specified, the initial date will be based on 0 in the UNIX epoch
  assert.strictEqual(Date.now(), 0);

  // Advance in time will also advance the date
  context.mock.timers.tick(9999);
  assert.strictEqual(Date.now(), 9999);
});const assert = require('node:assert');
const { test } = require('node:test');

test('mocks the Date object', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['Date'] });
  // If not specified, the initial date will be based on 0 in the UNIX epoch
  assert.strictEqual(Date.now(), 0);

  // Advance in time will also advance the date
  context.mock.timers.tick(9999);
  assert.strictEqual(Date.now(), 9999);
});

如果没有设置初始纪元时间,初始日期将基于 Unix 纪元中的 0。这是 1970 年 1 月 1 日 00:00:00 UTC。您可以通过将 now 属性传递给 .enable() 方法来设置初始日期。该值将用作模拟的 Date 对象的初始日期。它可以是正整数,也可以是另一个 Date 对象。

import assert from 'node:assert';
import { test } from 'node:test';

test('mocks the Date object with initial time', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['Date'], now: 100 });
  assert.strictEqual(Date.now(), 100);

  // Advance in time will also advance the date
  context.mock.timers.tick(200);
  assert.strictEqual(Date.now(), 300);
});const assert = require('node:assert');
const { test } = require('node:test');

test('mocks the Date object with initial time', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['Date'], now: 100 });
  assert.strictEqual(Date.now(), 100);

  // Advance in time will also advance the date
  context.mock.timers.tick(200);
  assert.strictEqual(Date.now(), 300);
});

您可以使用 .setTime() 方法手动将模拟日期移动到另一个时间。此方法只接受正整数。

注意: 此方法将执行任何模拟的、且时间早于新时间的定时器。

在下面的例子中,我们为模拟的日期设置了一个新的时间。

import assert from 'node:assert';
import { test } from 'node:test';

test('sets the time of a date object', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['Date'], now: 100 });
  assert.strictEqual(Date.now(), 100);

  // Advance in time will also advance the date
  context.mock.timers.setTime(1000);
  context.mock.timers.tick(200);
  assert.strictEqual(Date.now(), 1200);
});const assert = require('node:assert');
const { test } = require('node:test');

test('sets the time of a date object', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['Date'], now: 100 });
  assert.strictEqual(Date.now(), 100);

  // Advance in time will also advance the date
  context.mock.timers.setTime(1000);
  context.mock.timers.tick(200);
  assert.strictEqual(Date.now(), 1200);
});

如果您有任何设置为在过去运行的定时器,它将被执行,就像调用了 .tick() 方法一样。如果您想测试已经过去的、与时间相关的特性,这非常有用。

import assert from 'node:assert';
import { test } from 'node:test';

test('runs timers as setTime passes ticks', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const fn = context.mock.fn();
  setTimeout(fn, 1000);

  context.mock.timers.setTime(800);
  // Timer is not executed as the time is not yet reached
  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(Date.now(), 800);

  context.mock.timers.setTime(1200);
  // Timer is executed as the time is now reached
  assert.strictEqual(fn.mock.callCount(), 1);
  assert.strictEqual(Date.now(), 1200);
});const assert = require('node:assert');
const { test } = require('node:test');

test('runs timers as setTime passes ticks', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const fn = context.mock.fn();
  setTimeout(fn, 1000);

  context.mock.timers.setTime(800);
  // Timer is not executed as the time is not yet reached
  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(Date.now(), 800);

  context.mock.timers.setTime(1200);
  // Timer is executed as the time is now reached
  assert.strictEqual(fn.mock.callCount(), 1);
  assert.strictEqual(Date.now(), 1200);
});

使用 .runAll() 将执行队列中当前的所有定时器。这也会将模拟的日期推进到最后一个执行的定时器的时间,就像时间已经过去一样。

import assert from 'node:assert';
import { test } from 'node:test';

test('runs timers as setTime passes ticks', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const fn = context.mock.fn();
  setTimeout(fn, 1000);
  setTimeout(fn, 2000);
  setTimeout(fn, 3000);

  context.mock.timers.runAll();
  // All timers are executed as the time is now reached
  assert.strictEqual(fn.mock.callCount(), 3);
  assert.strictEqual(Date.now(), 3000);
});const assert = require('node:assert');
const { test } = require('node:test');

test('runs timers as setTime passes ticks', (context) => {
  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const fn = context.mock.fn();
  setTimeout(fn, 1000);
  setTimeout(fn, 2000);
  setTimeout(fn, 3000);

  context.mock.timers.runAll();
  // All timers are executed as the time is now reached
  assert.strictEqual(fn.mock.callCount(), 3);
  assert.strictEqual(Date.now(), 3000);
});

快照测试#

快照测试允许将任意值序列化为字符串值,并与一组已知的良好值进行比较。已知的良好值称为快照,并存储在快照文件中。快照文件由测试运行器管理,但设计为人类可读的,以帮助调试。最佳实践是将快照文件与您的测试文件一起检入到源代码控制中。

快照文件通过使用 --test-update-snapshots 命令行标志启动 Node.js 来生成。每个测试文件都会生成一个单独的快照文件。默认情况下,快照文件与测试文件同名,但文件扩展名为 .snapshot。可以使用 snapshot.setResolveSnapshotPath() 函数配置此行为。每个快照断言都对应于快照文件中的一个导出。

下面的例子展示了一个快照测试。第一次执行此测试时,它将失败,因为相应的快照文件不存在。

// test.js
suite('suite of snapshot tests', () => {
  test('snapshot test', (t) => {
    t.assert.snapshot({ value1: 1, value2: 2 });
    t.assert.snapshot(5);
  });
}); 

通过使用 --test-update-snapshots 运行测试文件来生成快照文件。测试应该通过,并且在与测试文件相同的目录中创建一个名为 test.js.snapshot 的文件。快照文件的内容如下所示。每个快照都由测试的完整名称和一个计数器来标识,以区分同一测试中的快照。

exports[`suite of snapshot tests > snapshot test 1`] = `
{
  "value1": 1,
  "value2": 2
}
`;

exports[`suite of snapshot tests > snapshot test 2`] = `
5
`; 

创建快照文件后,再次运行测试,而无需使用 --test-update-snapshots 标志。现在测试应该通过。

测试报告器#

node:test 模块支持传递 --test-reporter 标志,以便测试运行器使用特定的报告器。

支持以下内置报告器

  • spec spec 报告器以人类可读的格式输出测试结果。这是默认的报告器。

  • tap tap 报告器以 TAP 格式输出测试结果。

  • dot dot 报告器以紧凑的格式输出测试结果,其中每个通过的测试用 . 表示,每个失败的测试用 X 表示。

  • junit junit 报告器以 jUnit XML 格式输出测试结果

  • lcov 当与 --experimental-test-coverage 标志一起使用时,lcov 报告器输出测试覆盖率。

这些报告器的确切输出在 Node.js 的版本之间可能会发生变化,不应以编程方式依赖它们。如果需要以编程方式访问测试运行器的输出,请使用 <TestsStream> 发出的事件。

这些报告器可以通过 node:test/reporters 模块获得

import { tap, spec, dot, junit, lcov } from 'node:test/reporters';const { tap, spec, dot, junit, lcov } = require('node:test/reporters');

自定义报告器#

--test-reporter 可用于指定自定义报告器的路径。自定义报告器是一个模块,它导出一个被 stream.compose 接受的值。报告器应该转换 <TestsStream> 发出的事件

使用 <stream.Transform> 的自定义报告器示例

import { Transform } from 'node:stream';

const customReporter = new Transform({
  writableObjectMode: true,
  transform(event, encoding, callback) {
    switch (event.type) {
      case 'test:dequeue':
        callback(null, `test ${event.data.name} dequeued`);
        break;
      case 'test:enqueue':
        callback(null, `test ${event.data.name} enqueued`);
        break;
      case 'test:watch:drained':
        callback(null, 'test watch queue drained');
        break;
      case 'test:start':
        callback(null, `test ${event.data.name} started`);
        break;
      case 'test:pass':
        callback(null, `test ${event.data.name} passed`);
        break;
      case 'test:fail':
        callback(null, `test ${event.data.name} failed`);
        break;
      case 'test:plan':
        callback(null, 'test plan');
        break;
      case 'test:diagnostic':
      case 'test:stderr':
      case 'test:stdout':
        callback(null, event.data.message);
        break;
      case 'test:coverage': {
        const { totalLineCount } = event.data.summary.totals;
        callback(null, `total line count: ${totalLineCount}\n`);
        break;
      }
    }
  },
});

export default customReporter;const { Transform } = require('node:stream');

const customReporter = new Transform({
  writableObjectMode: true,
  transform(event, encoding, callback) {
    switch (event.type) {
      case 'test:dequeue':
        callback(null, `test ${event.data.name} dequeued`);
        break;
      case 'test:enqueue':
        callback(null, `test ${event.data.name} enqueued`);
        break;
      case 'test:watch:drained':
        callback(null, 'test watch queue drained');
        break;
      case 'test:start':
        callback(null, `test ${event.data.name} started`);
        break;
      case 'test:pass':
        callback(null, `test ${event.data.name} passed`);
        break;
      case 'test:fail':
        callback(null, `test ${event.data.name} failed`);
        break;
      case 'test:plan':
        callback(null, 'test plan');
        break;
      case 'test:diagnostic':
      case 'test:stderr':
      case 'test:stdout':
        callback(null, event.data.message);
        break;
      case 'test:coverage': {
        const { totalLineCount } = event.data.summary.totals;
        callback(null, `total line count: ${totalLineCount}\n`);
        break;
      }
    }
  },
});

module.exports = customReporter;

使用生成器函数的自定义报告器示例

export default async function * customReporter(source) {
  for await (const event of source) {
    switch (event.type) {
      case 'test:dequeue':
        yield `test ${event.data.name} dequeued\n`;
        break;
      case 'test:enqueue':
        yield `test ${event.data.name} enqueued\n`;
        break;
      case 'test:watch:drained':
        yield 'test watch queue drained\n';
        break;
      case 'test:start':
        yield `test ${event.data.name} started\n`;
        break;
      case 'test:pass':
        yield `test ${event.data.name} passed\n`;
        break;
      case 'test:fail':
        yield `test ${event.data.name} failed\n`;
        break;
      case 'test:plan':
        yield 'test plan\n';
        break;
      case 'test:diagnostic':
      case 'test:stderr':
      case 'test:stdout':
        yield `${event.data.message}\n`;
        break;
      case 'test:coverage': {
        const { totalLineCount } = event.data.summary.totals;
        yield `total line count: ${totalLineCount}\n`;
        break;
      }
    }
  }
}module.exports = async function * customReporter(source) {
  for await (const event of source) {
    switch (event.type) {
      case 'test:dequeue':
        yield `test ${event.data.name} dequeued\n`;
        break;
      case 'test:enqueue':
        yield `test ${event.data.name} enqueued\n`;
        break;
      case 'test:watch:drained':
        yield 'test watch queue drained\n';
        break;
      case 'test:start':
        yield `test ${event.data.name} started\n`;
        break;
      case 'test:pass':
        yield `test ${event.data.name} passed\n`;
        break;
      case 'test:fail':
        yield `test ${event.data.name} failed\n`;
        break;
      case 'test:plan':
        yield 'test plan\n';
        break;
      case 'test:diagnostic':
      case 'test:stderr':
      case 'test:stdout':
        yield `${event.data.message}\n`;
        break;
      case 'test:coverage': {
        const { totalLineCount } = event.data.summary.totals;
        yield `total line count: ${totalLineCount}\n`;
        break;
      }
    }
  }
};

提供给 --test-reporter 的值应该是一个字符串,就像在 JavaScript 代码的 import() 中使用的字符串,或者为 --import 提供的值。

多个报告器#

可以多次指定 --test-reporter 标志,以多种格式报告测试结果。在这种情况下,需要使用 --test-reporter-destination 为每个报告器指定一个目标。目标可以是 stdoutstderr 或文件路径。报告器和目标根据指定的顺序进行配对。

在下面的示例中,spec 报告器将输出到 stdout,而 dot 报告器将输出到 file.txt

node --test-reporter=spec --test-reporter=dot --test-reporter-destination=stdout --test-reporter-destination=file.txt 

当指定单个报告器时,目标将默认为 stdout,除非显式提供目标。

run([options])#

  • options <Object> 用于运行测试的配置选项。支持以下属性
    • concurrency <number> | <boolean> 如果提供了一个数字,那么将并行运行那么多个测试进程,其中每个进程对应一个测试文件。如果为 true,它将并行运行 os.availableParallelism() - 1 个测试文件。如果为 false,它将一次只运行一个测试文件。默认值: false
    • cwd: <string> 指定测试运行器要使用的当前工作目录。用作根据测试运行器执行模型解析文件的基本路径。默认值: process.cwd()
    • files: <Array> 包含要运行的文件列表的数组。默认值:来自 测试运行器执行模型 的匹配文件。
    • forceExit: <boolean> 配置测试运行器,以便在所有已知的测试都完成执行后退出进程,即使事件循环原本会保持活动状态。默认值: false
    • globPatterns: <Array> 包含用于匹配测试文件的 glob 模式列表的数组。此选项不能与 files 一起使用。默认值:来自 测试运行器执行模型 的匹配文件。
    • inspectPort <number> | <Function> 设置测试子进程的检查器端口。这可以是一个数字,也可以是一个不带参数并返回一个数字的函数。如果提供一个空值,则每个进程都获得自己的端口,从主进程的 process.debugPort 递增。如果 isolation 选项设置为 'none',则此选项将被忽略,因为不会生成任何子进程。默认值: undefined
    • isolation <string> 配置测试隔离的类型。如果设置为 'process',则每个测试文件都在一个单独的子进程中运行。如果设置为 'none',则所有测试文件都在当前进程中运行。默认值: 'process'
    • only: <boolean> 如果为真值,则测试上下文将只运行设置了 only 选项的测试
    • setup <Function> 接受 TestsStream 实例的函数,可用于在运行任何测试之前设置侦听器。默认值: undefined
    • execArgv <Array> 在生成子进程时,要传递给 node 可执行文件的 CLI 标志数组。当 isolation'none' 时,此选项无效。默认值: []
    • argv <Array> 在生成子进程时,要传递给每个测试文件的 CLI 标志数组。当 isolation'none' 时,此选项无效。默认值: []
    • signal <AbortSignal> 允许中止正在进行的测试执行。
    • testNamePatterns <string> | <RegExp> | <Array> 一个字符串、RegExp 或 RegExp 数组,可用于仅运行名称与提供的模式匹配的测试。测试名称模式被解释为 JavaScript 正则表达式。对于执行的每个测试,任何相应的测试钩子(例如 beforeEach())也会运行。默认值: undefined
    • testSkipPatterns <string> | <RegExp> | <Array> 一个字符串、RegExp 或 RegExp 数组,可用于排除运行名称与提供的模式匹配的测试。测试名称模式被解释为 JavaScript 正则表达式。对于执行的每个测试,任何相应的测试钩子(例如 beforeEach())也会运行。默认值: undefined
    • timeout <number> 测试执行失败的毫秒数。如果未指定,子测试将从其父测试继承此值。默认值: Infinity
    • watch <boolean> 是否以监听模式运行。默认值: false
    • shard <Object> 在特定分片中运行测试。默认值: undefined
      • index <number> 是介于 1 和 <total> 之间的正整数,用于指定要运行的分片的索引。 此选项是必需的
      • total <number> 是一个正整数,用于指定将测试文件拆分的总分片数。 此选项是必需的
    • coverage <boolean> 启用 代码覆盖率 收集。默认值: false
    • coverageExcludeGlobs <string> | <Array> 使用 glob 模式从代码覆盖率中排除特定文件,该模式可以匹配绝对和相对文件路径。 此属性仅在 coverage 设置为 true 时适用。 如果同时提供了 coverageExcludeGlobscoverageIncludeGlobs,则文件必须满足两个条件才能包含在覆盖率报告中。 默认值: undefined
    • coverageIncludeGlobs <string> | <Array> 使用 glob 模式在代码覆盖率中包含特定文件,该模式可以匹配绝对和相对文件路径。 此属性仅在 coverage 设置为 true 时适用。 如果同时提供了 coverageExcludeGlobscoverageIncludeGlobs,则文件必须满足两个条件才能包含在覆盖率报告中。 默认值: undefined
    • lineCoverage <number> 要求覆盖行的最小百分比。 如果代码覆盖率未达到指定的阈值,则该进程将以代码 1 退出。 默认值: 0
    • branchCoverage <number> 要求覆盖分支的最小百分比。 如果代码覆盖率未达到指定的阈值,则该进程将以代码 1 退出。 默认值: 0
    • functionCoverage <number> 要求覆盖函数的最小百分比。 如果代码覆盖率未达到指定的阈值,则该进程将以代码 1 退出。 默认值: 0
  • 返回:<TestsStream>

注意: shard 用于跨机器或进程水平并行化测试运行,非常适合跨各种环境的大规模执行。 它与 watch 模式不兼容,后者通过在文件更改时自动重新运行测试来针对快速代码迭代进行了定制。

import { tap } from 'node:test/reporters';
import { run } from 'node:test';
import process from 'node:process';
import path from 'node:path';

run({ files: [path.resolve('./tests/test.js')] })
 .on('test:fail', () => {
   process.exitCode = 1;
 })
 .compose(tap)
 .pipe(process.stdout);const { tap } = require('node:test/reporters');
const { run } = require('node:test');
const path = require('node:path');

run({ files: [path.resolve('./tests/test.js')] })
 .on('test:fail', () => {
   process.exitCode = 1;
 })
 .compose(tap)
 .pipe(process.stdout);

suite([name][, options][, fn])#

  • name <string> 套件的名称,在报告测试结果时显示。默认值: fnname 属性,如果 fn 没有名称,则为 '<anonymous>'
  • options <Object> 套件的可选配置选项。 这支持与 test([name][, options][, fn]) 相同的选项。
  • fn <Function> | <AsyncFunction> 声明嵌套测试和套件的套件函数。 此函数的第一个参数是 SuiteContext 对象。默认值: 无操作函数。

suite() 函数是从 node:test 模块导入的。

suite.skip([name][, options][, fn])#

跳过套件的简写。 这与 suite([name], { skip: true }[, fn]) 相同。

suite.todo([name][, options][, fn])#

将套件标记为 TODO 的简写。 这与 suite([name], { todo: true }[, fn]) 相同。

suite.only([name][, options][, fn])#

将套件标记为 only 的简写。 这与 suite([name], { only: true }[, fn]) 相同。

test([name][, options][, fn])#

  • name <string> 测试的名称,在报告测试结果时显示。默认值: fnname 属性,如果 fn 没有名称,则为 '<anonymous>'
  • options <Object> 测试的配置选项。 支持以下属性
    • concurrency <number> | <boolean> 如果提供一个数字,则该数量的测试将在应用程序线程中并行运行。 如果为 true,则所有计划的异步测试都在线程中并发运行。 如果为 false,则一次只运行一个测试。 如果未指定,子测试将从其父测试继承此值。默认值: false
    • only <boolean> 如果为真,并且测试上下文配置为仅运行 only 测试,则将运行此测试。 否则,将跳过该测试。默认值: false
    • signal <AbortSignal> 允许中止正在进行的测试。
    • skip <boolean> | <string> 如果为真,则跳过该测试。 如果提供了字符串,则该字符串将显示在测试结果中,作为跳过该测试的原因。默认值: false
    • todo <boolean> | <string> 如果为真,则将测试标记为 TODO。 如果提供了字符串,则该字符串将显示在测试结果中,作为测试为 TODO 的原因。默认值: false
    • timeout <number> 测试将在多少毫秒后失败。如果未指定,子测试将从其父测试继承此值。默认值: Infinity
    • plan <number> 预期在测试中运行的断言和子测试的数量。 如果测试中运行的断言数与计划中指定的数量不匹配,则测试将失败。默认值: undefined
  • fn <Function> | <AsyncFunction> 被测函数。 此函数的第一个参数是 TestContext 对象。 如果测试使用回调,则将回调函数作为第二个参数传递。默认值: 无操作函数。

test() 函数是从 test 模块导入的值。 每次调用此函数都会导致将测试报告给 <TestsStream>

传递给 fn 参数的 TestContext 对象可用于执行与当前测试相关的操作。 示例包括跳过测试、添加其他诊断信息或创建子测试。

如果测试完成时间超过 timeout 毫秒,则可以使用 timeout 选项使测试失败。 但是,这不是取消测试的可靠机制,因为正在运行的测试可能会阻止应用程序线程,从而阻止计划的取消。

test.skip([name][, options][, fn])#

跳过测试的简写,与 test([name], { skip: true }[, fn]) 相同。

test.todo([name][, options][, fn])#

将测试标记为 TODO 的简写,与 test([name], { todo: true }[, fn]) 相同。

test.only([name][, options][, fn])#

将测试标记为 only 的简写,与 test([name], { only: true }[, fn]) 相同。

describe([name][, options][, fn])#

别名:suite()

describe() 函数是从 node:test 模块导入的。

describe.skip([name][, options][, fn])#

跳过套件的简写。 这与 describe([name], { skip: true }[, fn]) 相同。

describe.todo([name][, options][, fn])#

将套件标记为 TODO 的简写。 这与 describe([name], { todo: true }[, fn]) 相同。

describe.only([name][, options][, fn])#

将套件标记为 only 的简写。 这与 describe([name], { only: true }[, fn]) 相同。

it([name][, options][, fn])#

别名:test()

it() 函数是从 node:test 模块导入的。

it.skip([name][, options][, fn])#

跳过测试的简写,与 it([name], { skip: true }[, fn]) 相同。

it.todo([name][, options][, fn])#

将测试标记为 TODO 的简写,与 it([name], { todo: true }[, fn]) 相同。

it.only([name][, options][, fn])#

将测试标记为 only 的简写,与 it([name], { only: true }[, fn]) 相同。

before([fn][, options])#

  • fn <Function> | <AsyncFunction> hook 函数。 如果 hook 使用回调函数,则回调函数作为第二个参数传递。 默认: 空操作函数。
  • options <Object> hook 的配置选项。 支持以下属性
    • signal <AbortSignal> 允许中止正在进行的 hook。
    • timeout <number> hook 在此毫秒数后将失败。 如果未指定,则子测试从其父级继承此值。 默认: Infinity

此函数创建一个在执行套件之前运行的 hook。

describe('tests', async () => {
  before(() => console.log('about to run some test'));
  it('is a subtest', () => {
    assert.ok('some relevant assertion here');
  });
}); 

after([fn][, options])#

  • fn <Function> | <AsyncFunction> hook 函数。 如果 hook 使用回调函数,则回调函数作为第二个参数传递。 默认: 空操作函数。
  • options <Object> hook 的配置选项。 支持以下属性
    • signal <AbortSignal> 允许中止正在进行的 hook。
    • timeout <number> hook 在此毫秒数后将失败。 如果未指定,则子测试从其父级继承此值。 默认: Infinity

此函数创建一个在执行套件之后运行的 hook。

describe('tests', async () => {
  after(() => console.log('finished running tests'));
  it('is a subtest', () => {
    assert.ok('some relevant assertion here');
  });
}); 

注意: 即使套件中的测试失败,也保证运行 after hook。

beforeEach([fn][, options])#

  • fn <Function> | <AsyncFunction> hook 函数。 如果 hook 使用回调函数,则回调函数作为第二个参数传递。 默认: 空操作函数。
  • options <Object> hook 的配置选项。 支持以下属性
    • signal <AbortSignal> 允许中止正在进行的 hook。
    • timeout <number> hook 在此毫秒数后将失败。 如果未指定,则子测试从其父级继承此值。 默认: Infinity

此函数创建一个在当前套件中的每个测试之前运行的 hook。

describe('tests', async () => {
  beforeEach(() => console.log('about to run a test'));
  it('is a subtest', () => {
    assert.ok('some relevant assertion here');
  });
}); 

afterEach([fn][, options])#

  • fn <Function> | <AsyncFunction> hook 函数。 如果 hook 使用回调函数,则回调函数作为第二个参数传递。 默认: 空操作函数。
  • options <Object> hook 的配置选项。 支持以下属性
    • signal <AbortSignal> 允许中止正在进行的 hook。
    • timeout <number> hook 在此毫秒数后将失败。 如果未指定,则子测试从其父级继承此值。 默认: Infinity

此函数创建一个在当前套件中的每个测试之后运行的 hook。 即使测试失败,也会运行 afterEach() hook。

describe('tests', async () => {
  afterEach(() => console.log('finished running a test'));
  it('is a subtest', () => {
    assert.ok('some relevant assertion here');
  });
}); 

assert#

一个对象,其方法用于配置当前进程中 TestContext 对象上可用的断言。 默认情况下,可以使用 node:assert 中的方法和快照测试功能。

可以通过将公共配置代码放置在使用 --require--import 预加载的模块中,将相同的配置应用于所有文件。

assert.register(name, fn)#

使用提供的名称和函数定义一个新的断言函数。 如果已存在具有相同名称的断言,则会覆盖它。

snapshot#

一个对象,其方法用于配置当前进程中的默认快照设置。 可以通过将公共配置代码放置在使用 --require--import 预加载的模块中,将相同的配置应用于所有文件。

snapshot.setDefaultSnapshotSerializers(serializers)#

  • serializers <Array> 用作快照测试的默认序列化器的同步函数数组。

此函数用于自定义测试运行器使用的默认序列化机制。 默认情况下,测试运行器通过在提供的值上调用 JSON.stringify(value, null, 2) 来执行序列化。 JSON.stringify() 在循环结构和支持的数据类型方面确实存在限制。 如果需要更强大的序列化机制,则应使用此函数。

snapshot.setResolveSnapshotPath(fn)#

  • fn <Function> 用于计算快照文件位置的函数。 该函数接收测试文件的路径作为其唯一参数。 如果测试未与文件关联(例如在 REPL 中),则输入未定义。 fn() 必须返回一个字符串,指定快照文件的位置。

此函数用于自定义用于快照测试的快照文件的位置。 默认情况下,快照文件名与入口点文件名相同,文件扩展名为 .snapshot

类: MockFunctionContext#

MockFunctionContext 类用于检查或操作通过 MockTracker API 创建的模拟的行为。

ctx.calls#

一个 getter,它返回用于跟踪对模拟的调用的内部数组的副本。 数组中的每个条目都是一个具有以下属性的对象。

  • arguments <Array> 传递给模拟函数的参数数组。
  • error <any> 如果模拟函数抛出,则此属性包含抛出的值。 默认: undefined
  • result <any> 模拟函数返回的值。
  • stack <Error> 一个 Error 对象,其堆栈可用于确定模拟函数调用的调用站点。
  • target <Function> | <undefined> 如果模拟函数是一个构造函数,则此字段包含正在构造的类。 否则,这将是 undefined
  • this <any> 模拟函数的 this 值。

ctx.callCount()#

  • 返回: <integer> 此模拟被调用的次数。

此函数返回此模拟被调用的次数。 此函数比检查 ctx.calls.length 更有效,因为 ctx.calls 是一个 getter,它创建内部调用跟踪数组的副本。

ctx.mockImplementation(implementation)#

此函数用于更改现有模拟的行为。

以下示例使用 t.mock.fn() 创建一个模拟函数,调用该模拟函数,然后将模拟实现更改为不同的函数。

test('changes a mock behavior', (t) => {
  let cnt = 0;

  function addOne() {
    cnt++;
    return cnt;
  }

  function addTwo() {
    cnt += 2;
    return cnt;
  }

  const fn = t.mock.fn(addOne);

  assert.strictEqual(fn(), 1);
  fn.mock.mockImplementation(addTwo);
  assert.strictEqual(fn(), 3);
  assert.strictEqual(fn(), 5);
}); 

ctx.mockImplementationOnce(implementation[, onCall])#

  • implementation <Function> | <AsyncFunction> 将用作 onCall 指定的调用号的模拟实现的函数。
  • onCall <integer> 将使用 implementation 的调用号。 如果指定的调用已经发生,则会抛出异常。 默认: 下一次调用的编号。

此函数用于为单个调用更改现有模拟的行为。 一旦发生调用 onCall,模拟将恢复为未使用 mockImplementationOnce() 时应使用的任何行为。

以下示例使用 t.mock.fn() 创建一个模拟函数,调用该模拟函数,将模拟实现更改为用于下一次调用的不同函数,然后恢复其先前的行为。

test('changes a mock behavior once', (t) => {
  let cnt = 0;

  function addOne() {
    cnt++;
    return cnt;
  }

  function addTwo() {
    cnt += 2;
    return cnt;
  }

  const fn = t.mock.fn(addOne);

  assert.strictEqual(fn(), 1);
  fn.mock.mockImplementationOnce(addTwo);
  assert.strictEqual(fn(), 3);
  assert.strictEqual(fn(), 4);
}); 

ctx.resetCalls()#

重置模拟函数的调用历史记录。

ctx.restore()#

将模拟函数的实现重置为其原始行为。 调用此函数后,仍然可以使用该模拟。

类: MockModuleContext#

稳定性: 1.0 - 早期开发

MockModuleContext 类用于操作通过 MockTracker API 创建的模块模拟的行为。

ctx.restore()#

重置模拟模块的实现。

类: MockTracker#

MockTracker 类用于管理模拟功能。 测试运行器模块提供一个顶级 mock 导出,它是一个 MockTracker 实例。 每个测试还通过测试上下文的 mock 属性提供自己的 MockTracker 实例。

mock.fn([original[, implementation]][, options])#

  • original <Function> | <AsyncFunction> 一个可选的函数,用于在其上创建模拟。 默认: 空操作函数。
  • implementation <Function> | <AsyncFunction> 一个可选的函数,用作 original 的模拟实现。 这对于创建在指定数量的调用中表现出一种行为,然后恢复 original 的行为的模拟非常有用。 默认:original 指定的函数。
  • options <Object> (可选)模拟函数的配置选项。支持以下属性:
    • times <integer> 模拟将使用 implementation 行为的次数。一旦模拟函数被调用 times 次,它将自动恢复 original 的行为。该值必须是大于零的整数。 默认值: Infinity
  • 返回值:<Proxy> 模拟函数。模拟函数包含一个特殊的 mock 属性,它是 MockFunctionContext 的一个实例,可用于检查和更改模拟函数的行为。

此函数用于创建模拟函数。

以下示例创建一个模拟函数,该函数在每次调用时将计数器加一。 times 选项用于修改模拟行为,使得前两次调用将计数器加二而不是加一。

test('mocks a counting function', (t) => {
  let cnt = 0;

  function addOne() {
    cnt++;
    return cnt;
  }

  function addTwo() {
    cnt += 2;
    return cnt;
  }

  const fn = t.mock.fn(addOne, addTwo, { times: 2 });

  assert.strictEqual(fn(), 2);
  assert.strictEqual(fn(), 4);
  assert.strictEqual(fn(), 5);
  assert.strictEqual(fn(), 6);
}); 

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

此函数是 MockTracker.method 的语法糖,并将 options.getter 设置为 true

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

  • object <Object> 其方法正在被模拟的对象。
  • methodName <string> | <symbol> 要模拟的 object 上的方法标识符。 如果 object[methodName] 不是函数,则会抛出错误。
  • implementation <Function> | <AsyncFunction> 用作 object[methodName] 的模拟实现的函数(可选)。默认值:object[methodName] 指定的原始方法。
  • options <Object> 模拟方法的可选配置选项。支持以下属性:
    • getter <boolean> 如果为 true,则 object[methodName] 被视为 getter。 此选项不能与 setter 选项一起使用。 默认值: false。
    • setter <boolean> 如果为 true,则 object[methodName] 被视为 setter。 此选项不能与 getter 选项一起使用。 默认值: false。
    • times <integer> 模拟将使用 implementation 行为的次数。一旦模拟方法被调用 times 次,它将自动恢复原始行为。该值必须是大于零的整数。 默认值: Infinity
  • 返回值:<Proxy> 模拟方法。 模拟方法包含一个特殊的 mock 属性,它是 MockFunctionContext 的一个实例,可用于检查和更改模拟方法的行为。

此函数用于在现有对象方法上创建模拟。 以下示例演示如何在现有对象方法上创建模拟。

test('spies on an object method', (t) => {
  const number = {
    value: 5,
    subtract(a) {
      return this.value - a;
    },
  };

  t.mock.method(number, 'subtract');
  assert.strictEqual(number.subtract.mock.callCount(), 0);
  assert.strictEqual(number.subtract(3), 2);
  assert.strictEqual(number.subtract.mock.callCount(), 1);

  const call = number.subtract.mock.calls[0];

  assert.deepStrictEqual(call.arguments, [3]);
  assert.strictEqual(call.result, 2);
  assert.strictEqual(call.error, undefined);
  assert.strictEqual(call.target, undefined);
  assert.strictEqual(call.this, number);
}); 

mock.module(specifier[, options])#

稳定性: 1.0 - 早期开发

  • specifier <string> | <URL> 标识要模拟的模块的字符串。
  • options <Object> 模拟模块的可选配置选项。支持以下属性:
    • cache <boolean> 如果为 false,则每次调用 require()import() 都会生成一个新的模拟模块。 如果为 true,则后续调用将返回相同的模块模拟,并且该模拟模块将插入到 CommonJS 缓存中。 默认值: false。
    • defaultExport <any> 用作模拟模块的默认导出的可选值。 如果未提供此值,则 ESM 模拟不包含默认导出。 如果模拟是 CommonJS 或内置模块,则此设置用作 module.exports 的值。 如果未提供此值,则 CJS 和内置模拟将使用空对象作为 module.exports 的值。
    • namedExports <Object> 一个可选对象,其键和值用于创建模拟模块的命名导出。 如果模拟是 CommonJS 或内置模块,则这些值将复制到 module.exports 上。 因此,如果使用命名导出和非对象默认导出创建模拟,则当该模拟用作 CJS 或内置模块时,将会抛出异常。
  • 返回值:<MockModuleContext> 可用于操作模拟的对象。

此函数用于模拟 ECMAScript 模块、CommonJS 模块、JSON 模块和 Node.js 内置模块的导出。 在模拟之前对原始模块的任何引用都不会受到影响。 为了启用模块模拟,必须使用 --experimental-test-module-mocks 命令行标志启动 Node.js。

以下示例演示如何为模块创建模拟。

test('mocks a builtin module in both module systems', async (t) => {
  // Create a mock of 'node:readline' with a named export named 'fn', which
  // does not exist in the original 'node:readline' module.
  const mock = t.mock.module('node:readline', {
    namedExports: { fn() { return 42; } },
  });

  let esmImpl = await import('node:readline');
  let cjsImpl = require('node:readline');

  // cursorTo() is an export of the original 'node:readline' module.
  assert.strictEqual(esmImpl.cursorTo, undefined);
  assert.strictEqual(cjsImpl.cursorTo, undefined);
  assert.strictEqual(esmImpl.fn(), 42);
  assert.strictEqual(cjsImpl.fn(), 42);

  mock.restore();

  // The mock is restored, so the original builtin module is returned.
  esmImpl = await import('node:readline');
  cjsImpl = require('node:readline');

  assert.strictEqual(typeof esmImpl.cursorTo, 'function');
  assert.strictEqual(typeof cjsImpl.cursorTo, 'function');
  assert.strictEqual(esmImpl.fn, undefined);
  assert.strictEqual(cjsImpl.fn, undefined);
}); 

mock.reset()#

此函数恢复所有先前由此 MockTracker 创建的模拟的默认行为,并将模拟与 MockTracker 实例分离。 一旦分离,模拟仍然可以使用,但 MockTracker 实例将无法再用于重置其行为或以其他方式与其交互。

每次测试完成后,都会在测试上下文的 MockTracker 上调用此函数。 如果广泛使用全局 MockTracker,建议手动调用此函数。

mock.restoreAll()#

此函数恢复所有先前由此 MockTracker 创建的模拟的默认行为。 与 mock.reset() 不同,mock.restoreAll() 不会将模拟与 MockTracker 实例分离。

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

此函数是 MockTracker.method 的语法糖,并将 options.setter 设置为 true

类: MockTimers#

模拟计时器是软件测试中常用的一种技术,用于模拟和控制计时器的行为,例如 setIntervalsetTimeout,而无需实际等待指定的时间间隔。

MockTimers 也可以模拟 Date 对象。

MockTracker 提供了一个顶级的 timers 导出,它是一个 MockTimers 实例。

timers.enable([enableOptions])#

为指定的定时器启用定时器模拟。

  • enableOptions <Object> 用于启用定时器模拟的可选配置选项。支持以下属性:
    • apis <Array> 一个可选数组,包含要模拟的定时器。 当前支持的计时器值为 'setInterval''setTimeout''setImmediate''Date'默认值: ['setInterval', 'setTimeout', 'setImmediate', 'Date']。 如果未提供数组,则所有与时间相关的 API('setInterval''clearInterval''setTimeout''clearTimeout''setImmediate''clearImmediate''Date')都将默认被模拟。
    • now <number> | <Date> 一个可选的数字或 Date 对象,表示用作 Date.now() 值的初始时间(以毫秒为单位)。 默认值: 0

注意: 当为特定定时器启用模拟时,其关联的清除函数也将被隐式模拟。

注意: 模拟 Date 会影响模拟定时器的行为,因为它们使用相同的内部时钟。

不设置初始时间的使用示例

import { mock } from 'node:test';
mock.timers.enable({ apis: ['setInterval'] });const { mock } = require('node:test');
mock.timers.enable({ apis: ['setInterval'] });

上面的示例启用了对 setInterval 定时器的模拟,并隐式模拟了 clearInterval 函数。 只有来自 node:timersnode:timers/promisesglobalThissetIntervalclearInterval 函数将被模拟。

设置初始时间的使用示例

import { mock } from 'node:test';
mock.timers.enable({ apis: ['Date'], now: 1000 });const { mock } = require('node:test');
mock.timers.enable({ apis: ['Date'], now: 1000 });

将初始 Date 对象用作时间设置的使用示例

import { mock } from 'node:test';
mock.timers.enable({ apis: ['Date'], now: new Date() });const { mock } = require('node:test');
mock.timers.enable({ apis: ['Date'], now: new Date() });

或者,如果调用没有带任何参数的 mock.timers.enable()

所有计时器('setInterval''clearInterval''setTimeout''clearTimeout''setImmediate''clearImmediate')都将被模拟。 来自 node:timersnode:timers/promisesglobalThissetIntervalclearIntervalsetTimeoutclearTimeoutsetImmediateclearImmediate 函数将被模拟。 以及全局 Date 对象。

timers.reset()#

此函数恢复所有先前由此 MockTimers 实例创建的模拟的默认行为,并将模拟与 MockTracker 实例分离。

注意: 每次测试完成后,都会在测试上下文的 MockTracker 上调用此函数。

import { mock } from 'node:test';
mock.timers.reset();const { mock } = require('node:test');
mock.timers.reset();

timers[Symbol.dispose]()#

调用 timers.reset()

timers.tick([milliseconds])#

推进所有模拟定时器的时间。

  • milliseconds <number> 要推进定时器的时间量(以毫秒为单位)。 默认值: 1

注意: 这与 Node.js 中 setTimeout 的行为不同,并且仅接受正数。 在 Node.js 中,仅出于 Web 兼容性原因才支持带有负数的 setTimeout

以下示例模拟一个 setTimeout 函数,并通过使用 .tick 来推进时间,从而触发所有挂起的定时器。

import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  context.mock.timers.enable({ apis: ['setTimeout'] });

  setTimeout(fn, 9999);

  assert.strictEqual(fn.mock.callCount(), 0);

  // Advance in time
  context.mock.timers.tick(9999);

  assert.strictEqual(fn.mock.callCount(), 1);
});const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();
  context.mock.timers.enable({ apis: ['setTimeout'] });

  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Advance in time
  context.mock.timers.tick(9999);

  assert.strictEqual(fn.mock.callCount(), 1);
});

或者,可以多次调用 .tick 函数

import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();
  context.mock.timers.enable({ apis: ['setTimeout'] });
  const nineSecs = 9000;
  setTimeout(fn, nineSecs);

  const threeSeconds = 3000;
  context.mock.timers.tick(threeSeconds);
  context.mock.timers.tick(threeSeconds);
  context.mock.timers.tick(threeSeconds);

  assert.strictEqual(fn.mock.callCount(), 1);
});const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();
  context.mock.timers.enable({ apis: ['setTimeout'] });
  const nineSecs = 9000;
  setTimeout(fn, nineSecs);

  const threeSeconds = 3000;
  context.mock.timers.tick(threeSeconds);
  context.mock.timers.tick(threeSeconds);
  context.mock.timers.tick(threeSeconds);

  assert.strictEqual(fn.mock.callCount(), 1);
});

如果 Date 也设置为模拟,则使用 .tick 推进时间也将推进在启用模拟后创建的任何 Date 对象的时间。

import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  setTimeout(fn, 9999);

  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(Date.now(), 0);

  // Advance in time
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
  assert.strictEqual(Date.now(), 9999);
});const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });

  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(Date.now(), 0);

  // Advance in time
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
  assert.strictEqual(Date.now(), 9999);
});
使用 clear 函数#

如前所述,来自定时器的所有 clear 函数 (clearTimeoutclearIntervalclearImmediate) 都会被隐式地模拟。看看这个使用 setTimeout 的例子

import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout'] });
  const id = setTimeout(fn, 9999);

  // Implicitly mocked as well
  clearTimeout(id);
  context.mock.timers.tick(9999);

  // As that setTimeout was cleared the mock function will never be called
  assert.strictEqual(fn.mock.callCount(), 0);
});const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout'] });
  const id = setTimeout(fn, 9999);

  // Implicitly mocked as well
  clearTimeout(id);
  context.mock.timers.tick(9999);

  // As that setTimeout was cleared the mock function will never be called
  assert.strictEqual(fn.mock.callCount(), 0);
});
使用 Node.js 定时器模块#

一旦你启用了模拟定时器,node:timersnode:timers/promises 模块,以及来自 Node.js 全局上下文的定时器都会被启用。

注意: 此 API 目前不支持解构函数,例如 import { setTimeout } from 'node:timers'

import assert from 'node:assert';
import { test } from 'node:test';
import nodeTimers from 'node:timers';
import nodeTimersPromises from 'node:timers/promises';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', async (context) => {
  const globalTimeoutObjectSpy = context.mock.fn();
  const nodeTimerSpy = context.mock.fn();
  const nodeTimerPromiseSpy = context.mock.fn();

  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(globalTimeoutObjectSpy, 9999);
  nodeTimers.setTimeout(nodeTimerSpy, 9999);

  const promise = nodeTimersPromises.setTimeout(9999).then(nodeTimerPromiseSpy);

  // Advance in time
  context.mock.timers.tick(9999);
  assert.strictEqual(globalTimeoutObjectSpy.mock.callCount(), 1);
  assert.strictEqual(nodeTimerSpy.mock.callCount(), 1);
  await promise;
  assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1);
});const assert = require('node:assert');
const { test } = require('node:test');
const nodeTimers = require('node:timers');
const nodeTimersPromises = require('node:timers/promises');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', async (context) => {
  const globalTimeoutObjectSpy = context.mock.fn();
  const nodeTimerSpy = context.mock.fn();
  const nodeTimerPromiseSpy = context.mock.fn();

  // Optionally choose what to mock
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(globalTimeoutObjectSpy, 9999);
  nodeTimers.setTimeout(nodeTimerSpy, 9999);

  const promise = nodeTimersPromises.setTimeout(9999).then(nodeTimerPromiseSpy);

  // Advance in time
  context.mock.timers.tick(9999);
  assert.strictEqual(globalTimeoutObjectSpy.mock.callCount(), 1);
  assert.strictEqual(nodeTimerSpy.mock.callCount(), 1);
  await promise;
  assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1);
});

在 Node.js 中,来自 node:timers/promisessetInterval 是一个 AsyncGenerator,并且也被此 API 支持。

import assert from 'node:assert';
import { test } from 'node:test';
import nodeTimersPromises from 'node:timers/promises';
test('should tick five times testing a real use case', async (context) => {
  context.mock.timers.enable({ apis: ['setInterval'] });

  const expectedIterations = 3;
  const interval = 1000;
  const startedAt = Date.now();
  async function run() {
    const times = [];
    for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) {
      times.push(time);
      if (times.length === expectedIterations) break;
    }
    return times;
  }

  const r = run();
  context.mock.timers.tick(interval);
  context.mock.timers.tick(interval);
  context.mock.timers.tick(interval);

  const timeResults = await r;
  assert.strictEqual(timeResults.length, expectedIterations);
  for (let it = 1; it < expectedIterations; it++) {
    assert.strictEqual(timeResults[it - 1], startedAt + (interval * it));
  }
});const assert = require('node:assert');
const { test } = require('node:test');
const nodeTimersPromises = require('node:timers/promises');
test('should tick five times testing a real use case', async (context) => {
  context.mock.timers.enable({ apis: ['setInterval'] });

  const expectedIterations = 3;
  const interval = 1000;
  const startedAt = Date.now();
  async function run() {
    const times = [];
    for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) {
      times.push(time);
      if (times.length === expectedIterations) break;
    }
    return times;
  }

  const r = run();
  context.mock.timers.tick(interval);
  context.mock.timers.tick(interval);
  context.mock.timers.tick(interval);

  const timeResults = await r;
  assert.strictEqual(timeResults.length, expectedIterations);
  for (let it = 1; it < expectedIterations; it++) {
    assert.strictEqual(timeResults[it - 1], startedAt + (interval * it));
  }
});

timers.runAll()#

立即触发所有待处理的模拟定时器。如果 Date 对象也被模拟,它也会将 Date 对象推进到最远的定时器的时间。

下面的例子立即触发所有待处理的定时器,导致它们没有任何延迟地执行。

import assert from 'node:assert';
import { test } from 'node:test';

test('runAll functions following the given order', (context) => {
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const results = [];
  setTimeout(() => results.push(1), 9999);

  // Notice that if both timers have the same timeout,
  // the order of execution is guaranteed
  setTimeout(() => results.push(3), 8888);
  setTimeout(() => results.push(2), 8888);

  assert.deepStrictEqual(results, []);

  context.mock.timers.runAll();
  assert.deepStrictEqual(results, [3, 2, 1]);
  // The Date object is also advanced to the furthest timer's time
  assert.strictEqual(Date.now(), 9999);
});const assert = require('node:assert');
const { test } = require('node:test');

test('runAll functions following the given order', (context) => {
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const results = [];
  setTimeout(() => results.push(1), 9999);

  // Notice that if both timers have the same timeout,
  // the order of execution is guaranteed
  setTimeout(() => results.push(3), 8888);
  setTimeout(() => results.push(2), 8888);

  assert.deepStrictEqual(results, []);

  context.mock.timers.runAll();
  assert.deepStrictEqual(results, [3, 2, 1]);
  // The Date object is also advanced to the furthest timer's time
  assert.strictEqual(Date.now(), 9999);
});

注意: runAll() 函数专门设计用于在定时器模拟的上下文中触发定时器。它对实时系统时钟或模拟环境之外的实际定时器没有任何影响。

timers.setTime(milliseconds)#

设置当前的 Unix 时间戳,它将用作任何模拟 Date 对象的参考。

import assert from 'node:assert';
import { test } from 'node:test';

test('runAll functions following the given order', (context) => {
  const now = Date.now();
  const setTime = 1000;
  // Date.now is not mocked
  assert.deepStrictEqual(Date.now(), now);

  context.mock.timers.enable({ apis: ['Date'] });
  context.mock.timers.setTime(setTime);
  // Date.now is now 1000
  assert.strictEqual(Date.now(), setTime);
});const assert = require('node:assert');
const { test } = require('node:test');

test('setTime replaces current time', (context) => {
  const now = Date.now();
  const setTime = 1000;
  // Date.now is not mocked
  assert.deepStrictEqual(Date.now(), now);

  context.mock.timers.enable({ apis: ['Date'] });
  context.mock.timers.setTime(setTime);
  // Date.now is now 1000
  assert.strictEqual(Date.now(), setTime);
});
日期和定时器协同工作#

日期和定时器对象相互依赖。 如果你使用 setTime() 将当前时间传递给模拟的 Date 对象,则使用 setTimeoutsetInterval 设置的定时器将不会受到影响。

但是,tick 方法推进模拟的 Date 对象。

import assert from 'node:assert';
import { test } from 'node:test';

test('runAll functions following the given order', (context) => {
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const results = [];
  setTimeout(() => results.push(1), 9999);

  assert.deepStrictEqual(results, []);
  context.mock.timers.setTime(12000);
  assert.deepStrictEqual(results, []);
  // The date is advanced but the timers don't tick
  assert.strictEqual(Date.now(), 12000);
});const assert = require('node:assert');
const { test } = require('node:test');

test('runAll functions following the given order', (context) => {
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const results = [];
  setTimeout(() => results.push(1), 9999);

  assert.deepStrictEqual(results, []);
  context.mock.timers.setTime(12000);
  assert.deepStrictEqual(results, []);
  // The date is advanced but the timers don't tick
  assert.strictEqual(Date.now(), 12000);
});

类: TestsStream#

成功调用 run() 方法将返回一个新的 <TestsStream> 对象,该对象流式传输一系列代表测试执行的事件。TestsStream 将按照测试定义的顺序发出事件

一些事件保证以与测试定义相同的顺序发出,而另一些事件则以测试执行的顺序发出。

事件: 'test:coverage'#

  • data <Object>
    • summary <Object> 一个包含覆盖率报告的对象。
      • files <Array> 一个包含各个文件的覆盖率报告的数组。每个报告都是一个具有以下模式的对象
        • path <string> 文件的绝对路径。
        • totalLineCount <number> 总行数。
        • totalBranchCount <number> 总分支数。
        • totalFunctionCount <number> 总函数数。
        • coveredLineCount <number> 覆盖的行数。
        • coveredBranchCount <number> 覆盖的分支数。
        • coveredFunctionCount <number> 覆盖的函数数。
        • coveredLinePercent <number> 行覆盖百分比。
        • coveredBranchPercent <number> 分支覆盖百分比。
        • coveredFunctionPercent <number> 函数覆盖百分比。
        • functions <Array> 一个代表函数覆盖率的函数数组。
          • name <string> 函数的名称。
          • line <number> 定义该函数的行号。
          • count <number> 该函数被调用的次数。
        • branches <Array> 一个代表分支覆盖率的分支数组。
          • line <number> 定义该分支的行号。
          • count <number> 该分支被执行的次数。
        • lines <Array> 一个代表行号以及它们被覆盖的次数的行数组。
      • thresholds <Object> 一个包含每种覆盖类型是否达标的对象。
      • totals <Object> 一个包含所有文件的覆盖率摘要的对象。
        • totalLineCount <number> 总行数。
        • totalBranchCount <number> 总分支数。
        • totalFunctionCount <number> 总函数数。
        • coveredLineCount <number> 覆盖的行数。
        • coveredBranchCount <number> 覆盖的分支数。
        • coveredFunctionCount <number> 覆盖的函数数。
        • coveredLinePercent <number> 行覆盖百分比。
        • coveredBranchPercent <number> 分支覆盖百分比。
        • coveredFunctionPercent <number> 函数覆盖百分比。
      • workingDirectory <string> 代码覆盖率开始时的工作目录。 这对于在测试更改了 Node.js 进程的工作目录的情况下显示相对路径名很有用。
    • nesting <number> 测试的嵌套级别。

当代码覆盖率启用且所有测试都已完成时发出。

事件: 'test:complete'#

当测试完成其执行时发出。 此事件不会以与测试定义相同的顺序发出。 相应的声明顺序事件是 'test:pass''test:fail'

事件: 'test:dequeue'#

  • data <Object>
    • column <number> | <undefined> 定义测试的列号,如果测试是通过 REPL 运行的,则为 undefined
    • file <string> | <undefined> 测试文件的路径,如果测试是通过 REPL 运行的,则为 undefined
    • line <number> | <undefined> 定义测试的行号,如果测试是通过 REPL 运行的,则为 undefined
    • name <string> 测试名称。
    • nesting <number> 测试的嵌套级别。
    • type <string> 测试类型。可以是 'suite''test'

当测试被移出队列,即将执行时触发。不保证此事件的触发顺序与测试定义顺序相同。相应的声明顺序事件是 'test:start'

事件: 'test:diagnostic'#

  • data <Object>
    • column <number> | <undefined> 定义测试的列号,如果测试是通过 REPL 运行的,则为 undefined
    • file <string> | <undefined> 测试文件的路径,如果测试是通过 REPL 运行的,则为 undefined
    • line <number> | <undefined> 定义测试的行号,如果测试是通过 REPL 运行的,则为 undefined
    • message <string> 诊断消息。
    • nesting <number> 测试的嵌套级别。

当调用 context.diagnostic 时触发。保证此事件的触发顺序与测试定义顺序相同。

事件: 'test:enqueue'#

  • data <Object>
    • column <number> | <undefined> 定义测试的列号,如果测试是通过 REPL 运行的,则为 undefined
    • file <string> | <undefined> 测试文件的路径,如果测试是通过 REPL 运行的,则为 undefined
    • line <number> | <undefined> 定义测试的行号,如果测试是通过 REPL 运行的,则为 undefined
    • name <string> 测试名称。
    • nesting <number> 测试的嵌套级别。
    • type <string> 测试类型。可以是 'suite''test'

当一个测试被加入执行队列时触发。

事件: 'test:fail'#

当测试失败时触发。保证此事件的触发顺序与测试定义顺序相同。相应的执行顺序事件是 'test:complete'

事件: 'test:pass'#

当测试通过时触发。保证此事件的触发顺序与测试定义顺序相同。相应的执行顺序事件是 'test:complete'

事件: 'test:plan'#

  • data <Object>
    • column <number> | <undefined> 定义测试的列号,如果测试是通过 REPL 运行的,则为 undefined
    • file <string> | <undefined> 测试文件的路径,如果测试是通过 REPL 运行的,则为 undefined
    • line <number> | <undefined> 定义测试的行号,如果测试是通过 REPL 运行的,则为 undefined
    • nesting <number> 测试的嵌套级别。
    • count <number> 已经运行的子测试的数量。

当给定测试的所有子测试都完成时触发。保证此事件的触发顺序与测试定义顺序相同。

事件: 'test:start'#

  • data <Object>
    • column <number> | <undefined> 定义测试的列号,如果测试是通过 REPL 运行的,则为 undefined
    • file <string> | <undefined> 测试文件的路径,如果测试是通过 REPL 运行的,则为 undefined
    • line <number> | <undefined> 定义测试的行号,如果测试是通过 REPL 运行的,则为 undefined
    • name <string> 测试名称。
    • nesting <number> 测试的嵌套级别。

当一个测试开始报告自身及其子测试的状态时触发。保证此事件的触发顺序与测试定义顺序相同。相应的执行顺序事件是 'test:dequeue'

事件: 'test:stderr'#

当正在运行的测试写入到 stderr 时触发。只有当传递 --test 标志时才会触发此事件。不保证此事件的触发顺序与测试定义顺序相同。

事件: 'test:stdout'#

当正在运行的测试写入到 stdout 时触发。只有当传递 --test 标志时才会触发此事件。不保证此事件的触发顺序与测试定义顺序相同。

事件: 'test:summary'#

  • data <Object>
    • counts <Object> 一个包含各种测试结果计数的对象。
      • cancelled <number> 取消的测试总数。
      • failed <number> 失败的测试总数。
      • passed <number> 通过的测试总数。
      • skipped <number> 跳过的测试总数。
      • suites <number> 运行的测试套件总数。
      • tests <number> 运行的测试总数,不包括测试套件。
      • todo <number> TODO 测试的总数。
      • topLevel <number> 顶层测试和测试套件的总数。
    • duration_ms <number> 测试运行的持续时间,以毫秒为单位。
    • file <string> | <undefined> 生成摘要的测试文件的路径。如果摘要对应于多个文件,则此值为 undefined
    • success <boolean> 指示测试运行是否被认为是成功的。如果发生任何错误情况,例如测试失败或未达到覆盖率阈值,则此值将被设置为 false

当测试运行完成时触发。此事件包含与已完成的测试运行相关的指标,可用于确定测试运行是通过还是失败。如果使用进程级别的测试隔离,则除了最终的累积摘要之外,还会为每个测试文件生成一个 'test:summary' 事件。

事件: 'test:watch:drained'#

当在监听模式下没有更多的测试被加入执行队列时触发。

类: TestContext#

TestContext 的实例被传递给每个测试函数,以便与测试运行器进行交互。但是,TestContext 构造函数不作为 API 的一部分公开。

context.before([fn][, options])#

  • fn <Function> | <AsyncFunction> Hook 函数。此函数的第一个参数是一个 TestContext 对象。如果 Hook 使用回调,则回调函数作为第二个参数传递。默认值: 一个空操作函数。
  • options <Object> hook 的配置选项。 支持以下属性
    • signal <AbortSignal> 允许中止正在进行的 hook。
    • timeout <number> hook 在此毫秒数后将失败。 如果未指定,则子测试从其父级继承此值。 默认: Infinity

此函数用于创建一个在当前测试的子测试之前运行的 Hook。

context.beforeEach([fn][, options])#

  • fn <Function> | <AsyncFunction> Hook 函数。此函数的第一个参数是一个 TestContext 对象。如果 Hook 使用回调,则回调函数作为第二个参数传递。默认值: 一个空操作函数。
  • options <Object> hook 的配置选项。 支持以下属性
    • signal <AbortSignal> 允许中止正在进行的 hook。
    • timeout <number> hook 在此毫秒数后将失败。 如果未指定,则子测试从其父级继承此值。 默认: Infinity

此函数用于创建一个在当前测试的每个子测试之前运行的 Hook。

test('top level test', async (t) => {
  t.beforeEach((t) => t.diagnostic(`about to run ${t.name}`));
  t.test('This is a subtest', (t) => {
    assert.ok('some relevant assertion here');
  });
}); 

context.after([fn][, options])#

  • fn <Function> | <AsyncFunction> Hook 函数。此函数的第一个参数是一个 TestContext 对象。如果 Hook 使用回调,则回调函数作为第二个参数传递。默认值: 一个空操作函数。
  • options <Object> hook 的配置选项。 支持以下属性
    • signal <AbortSignal> 允许中止正在进行的 hook。
    • timeout <number> hook 在此毫秒数后将失败。 如果未指定,则子测试从其父级继承此值。 默认: Infinity

此函数用于创建一个在当前测试完成后运行的 Hook。

test('top level test', async (t) => {
  t.after((t) => t.diagnostic(`finished running ${t.name}`));
  assert.ok('some relevant assertion here');
}); 

context.afterEach([fn][, options])#

  • fn <Function> | <AsyncFunction> Hook 函数。此函数的第一个参数是一个 TestContext 对象。如果 Hook 使用回调,则回调函数作为第二个参数传递。默认值: 一个空操作函数。
  • options <Object> hook 的配置选项。 支持以下属性
    • signal <AbortSignal> 允许中止正在进行的 hook。
    • timeout <number> hook 在此毫秒数后将失败。 如果未指定,则子测试从其父级继承此值。 默认: Infinity

此函数用于创建一个在当前测试的每个子测试之后运行的 Hook。

test('top level test', async (t) => {
  t.afterEach((t) => t.diagnostic(`finished running ${t.name}`));
  t.test('This is a subtest', (t) => {
    assert.ok('some relevant assertion here');
  });
}); 

context.assert#

一个包含绑定到 context 的断言方法的对象。node:assert 模块中的顶层函数在此处公开,目的是创建测试计划。

test('test', (t) => {
  t.plan(1);
  t.assert.strictEqual(true, true);
}); 
context.assert.fileSnapshot(value, path[, options])#
  • value <any> 要序列化为字符串的值。如果 Node.js 使用 --test-update-snapshots 标志启动,则序列化的值将被写入 path。否则,序列化的值将与现有快照文件的内容进行比较。
  • path <string> 序列化的 value 写入的文件。
  • options <Object> 可选的配置选项。支持以下属性:
    • serializers <Array> 用于将 value 序列化为字符串的同步函数数组。value 作为第一个序列化函数的唯一参数传递。每个序列化函数的返回值作为输入传递给下一个序列化函数。一旦所有序列化程序都已运行,则将结果值强制转换为字符串。默认值: 如果未提供序列化程序,则使用测试运行器的默认序列化程序。

此函数序列化 value 并将其写入由 path 指定的文件。

test('snapshot test with default serialization', (t) => {
  t.assert.fileSnapshot({ value1: 1, value2: 2 }, './snapshots/snapshot.json');
}); 

此函数与 context.assert.snapshot() 的不同之处在于:

  • 快照文件路径由用户显式提供。
  • 每个快照文件仅限于单个快照值。
  • 测试运行器不执行额外的转义。

这些差异允许快照文件更好地支持语法突出显示等功能。

context.assert.snapshot(value[, options])#
  • value <any> 要序列化为字符串的值。如果 Node.js 启动时带有 --test-update-snapshots 标志,则序列化后的值会被写入快照文件。否则,序列化后的值会与现有快照文件中对应的值进行比较。
  • options <Object> 可选的配置选项。支持以下属性:
    • serializers <Array> 用于将 value 序列化为字符串的同步函数数组。value 作为第一个序列化函数的唯一参数传递。每个序列化函数的返回值作为输入传递给下一个序列化函数。一旦所有序列化程序都已运行,则将结果值强制转换为字符串。默认值: 如果未提供序列化程序,则使用测试运行器的默认序列化程序。

此函数实现了快照测试的断言。

test('snapshot test with default serialization', (t) => {
  t.assert.snapshot({ value1: 1, value2: 2 });
});

test('snapshot test with custom serialization', (t) => {
  t.assert.snapshot({ value3: 3, value4: 4 }, {
    serializers: [(value) => JSON.stringify(value)],
  });
}); 

context.diagnostic(message)#

  • message <string> 要报告的消息。

此函数用于将诊断信息写入输出。任何诊断信息都包含在测试结果的末尾。此函数不返回值。

test('top level test', (t) => {
  t.diagnostic('A diagnostic message');
}); 

context.filePath#

创建当前测试的测试文件的绝对路径。如果测试文件导入了生成测试的附加模块,则导入的测试将返回根测试文件的路径。

context.fullName#

测试的名称以及其每个祖先的名称,用 > 分隔。

context.name#

测试的名称。

context.plan(count[,options])#

  • count <number> 期望运行的断言和子测试的数量。
  • options <Object> 计划的其他选项。
    • wait <boolean> | <number> 计划的等待时间
      • 如果为 true,则计划无限期地等待所有断言和子测试运行。
      • 如果为 false,则计划在测试函数完成后立即进行检查,而不等待任何挂起的断言或子测试。在此检查之后完成的任何断言或子测试将不计入计划。
      • 如果是一个数字,则指定在等待匹配预期断言和子测试时超时的最大等待时间(以毫秒为单位)。如果达到超时时间,测试将失败。 默认值: false

此函数用于设置测试中预期运行的断言和子测试的数量。 如果运行的断言和子测试的数量与预期计数不匹配,则测试将失败。

注意:为了确保跟踪断言,必须使用 t.assert 而不是直接使用 assert

test('top level test', (t) => {
  t.plan(2);
  t.assert.ok('some relevant assertion here');
  t.test('subtest', () => {});
}); 

当使用异步代码时,可以使用 plan 函数来确保运行正确的断言数量

test('planning with streams', (t, done) => {
  function* generate() {
    yield 'a';
    yield 'b';
    yield 'c';
  }
  const expected = ['a', 'b', 'c'];
  t.plan(expected.length);
  const stream = Readable.from(generate());
  stream.on('data', (chunk) => {
    t.assert.strictEqual(chunk, expected.shift());
  });

  stream.on('end', () => {
    done();
  });
}); 

当使用 wait 选项时,您可以控制测试等待预期断言的时间。例如,设置最大等待时间可确保测试将在指定的时间范围内等待异步断言完成

test('plan with wait: 2000 waits for async assertions', (t) => {
  t.plan(1, { wait: 2000 }); // Waits for up to 2 seconds for the assertion to complete.

  const asyncActivity = () => {
    setTimeout(() => {
      t.assert.ok(true, 'Async assertion completed within the wait time');
    }, 1000); // Completes after 1 second, within the 2-second wait time.
  };

  asyncActivity(); // The test will pass because the assertion is completed in time.
}); 

注意:如果指定了 wait 超时,则仅在测试函数执行完毕后才开始倒计时。

context.runOnly(shouldRunOnlyTests)#

  • shouldRunOnlyTests <boolean> 是否运行 only 测试。

如果 shouldRunOnlyTests 为真值,则测试上下文将仅运行设置了 only 选项的测试。 否则,将运行所有测试。 如果 Node.js 启动时没有使用 --test-only 命令行选项,则此函数不执行任何操作。

test('top level test', (t) => {
  // The test context can be set to run subtests with the 'only' option.
  t.runOnly(true);
  t.test('this subtest is now skipped');
  t.test('this subtest is run', { only: true });
}); 

context.signal#

可用于在测试中止时中止测试子任务。

test('top level test', async (t) => {
  await fetch('some/uri', { signal: t.signal });
}); 

context.skip([message])#

  • message <string> 可选的跳过消息。

此函数使测试的输出指示该测试已跳过。 如果提供了 message,则将其包含在输出中。 调用 skip() 不会终止测试函数的执行。 此函数不返回值。

test('top level test', (t) => {
  // Make sure to return here as well if the test contains additional logic.
  t.skip('this is skipped');
}); 

context.todo([message])#

  • message <string> 可选的 TODO 消息。

此函数将 TODO 指令添加到测试的输出中。 如果提供了 message,则将其包含在输出中。 调用 todo() 不会终止测试函数的执行。 此函数不返回值。

test('top level test', (t) => {
  // This test is marked as `TODO`
  t.todo('this is a todo');
}); 

context.test([name][, options][, fn])#

  • name <string> 子测试的名称,该名称在报告测试结果时显示。 默认值: fnname 属性,如果 fn 没有名称,则为 '<anonymous>'
  • options <Object> 子测试的配置选项。 支持以下属性:
    • concurrency <number> | <boolean> | <null> 如果提供一个数字,则将在应用程序线程中并行运行该数量的测试。 如果为 true,它将并行运行所有子测试。 如果为 false,它将一次只运行一个测试。 如果未指定,则子测试从其父测试继承此值。 默认值: null
    • only <boolean> 如果为真,并且测试上下文配置为仅运行 only 测试,则将运行此测试。 否则,将跳过该测试。默认值: false
    • signal <AbortSignal> 允许中止正在进行的测试。
    • skip <boolean> | <string> 如果为真,则跳过该测试。 如果提供了字符串,则该字符串将显示在测试结果中,作为跳过该测试的原因。默认值: false
    • todo <boolean> | <string> 如果为真,则将测试标记为 TODO。 如果提供了字符串,则该字符串将显示在测试结果中,作为测试为 TODO 的原因。默认值: false
    • timeout <number> 测试将在多少毫秒后失败。如果未指定,子测试将从其父测试继承此值。默认值: Infinity
    • plan <number> 预期在测试中运行的断言和子测试的数量。 如果测试中运行的断言数与计划中指定的数量不匹配,则测试将失败。默认值: undefined
  • fn <Function> | <AsyncFunction> 被测函数。 此函数的第一个参数是 TestContext 对象。 如果测试使用回调,则将回调函数作为第二个参数传递。默认值: 无操作函数。

此函数用于在当前测试下创建子测试。 此函数的行为方式与顶级 test() 函数相同。

test('top level test', async (t) => {
  t.test(
    'This is a subtest',
    { only: false, skip: false, concurrency: 1, todo: false, plan: 1 },
    (t) => {
      t.assert.ok('some relevant assertion here');
    },
  );
}); 

context.waitFor(condition[, options])#

  • condition <Function> | <AsyncFunction> 一个断言函数,它会定期调用,直到成功完成或定义的轮询超时时间过去。 成功完成定义为不抛出或拒绝。 此函数不接受任何参数,并且允许返回任何值。
  • options <Object> 轮询操作的可选配置对象。 支持以下属性:
    • interval <number> 在不成功调用 condition 之后,再次尝试之前要等待的毫秒数。 默认值: 50
    • timeout <number> 轮询超时时间(以毫秒为单位)。 如果 condition 在此时间过去之前未成功,则会发生错误。 默认值: 1000
  • 返回: <Promise> 使用 condition 返回的值实现。

此方法轮询 condition 函数,直到该函数成功返回或操作超时。

类: SuiteContext#

SuiteContext 的实例会传递给每个套件函数,以便与测试运行器进行交互。 但是,SuiteContext 构造函数不会作为 API 的一部分公开。

context.filePath#

创建当前套件的测试文件的绝对路径。 如果测试文件导入了生成套件的附加模块,则导入的套件将返回根测试文件的路径。

context.name#

套件的名称。

context.signal#

可用于在测试中止时中止测试子任务。