Skip to content

测试运行器

[历史]

版本变更
v20.0.0测试运行器现已稳定。
v18.0.0, v16.17.0新增于:v18.0.0, v16.17.0

[稳定:2 - 稳定]

稳定:2 稳定性:2 - 稳定

源代码: lib/test.js

node:test 模块方便创建 JavaScript 测试。访问方式:

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

此模块仅在 node: 模式下可用。

通过 test 模块创建的测试包含单个函数,该函数以三种方式之一进行处理:

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

js
test('同步通过测试', t => {
  // 此测试通过,因为它没有抛出异常。
  assert.strictEqual(1, 1)
})

test('同步失败测试', t => {
  // 此测试失败,因为它抛出异常。
  assert.strictEqual(1, 2)
})

test('异步通过测试', async t => {
  // 此测试通过,因为异步函数返回的 Promise 已完成且未被拒绝。
  assert.strictEqual(1, 1)
})

test('异步失败测试', async t => {
  // 此测试失败,因为异步函数返回的 Promise 被拒绝了。
  assert.strictEqual(1, 2)
})

test('使用 Promise 的失败测试', t => {
  // Promise 也可直接使用。
  return new Promise((resolve, reject) => {
    setImmediate(() => {
      reject(new Error('这将导致测试失败'))
    })
  })
})

test('回调通过测试', (t, done) => {
  // done() 是回调函数。当 setImmediate() 运行时,它会调用 done() 且不带任何参数。
  setImmediate(done)
})

test('回调失败测试', (t, done) => {
  // 当 setImmediate() 运行时,done() 会使用 Error 对象被调用,并且测试失败。
  setImmediate(() => {
    done(new Error('回调失败'))
  })
})

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

子测试

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

js
test('顶级测试', async t => {
  await t.test('子测试 1', t => {
    assert.strictEqual(1, 1)
  })

  await t.test('子测试 2', t => {
    assert.strictEqual(2, 2)
  })
})

在此示例中,使用 await 来确保两个子测试都已完成。这是必要的,因为测试不会等待其子测试完成,这与在套件中创建的测试不同。当父测试完成时,任何仍未完成的子测试都会被取消并视为失败。任何子测试失败都会导致父测试失败。

跳过测试

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

js
// 使用 skip 选项,但未提供消息。
test('skip 选项', { skip: true }, t => {
  // 此代码永远不会执行。
})

// 使用 skip 选项,并提供消息。
test('带有消息的 skip 选项', { skip: 'this is skipped' }, t => {
  // 此代码永远不会执行。
})

test('skip() 方法', t => {
  // 如果测试包含其他逻辑,也请确保在此处返回。
  t.skip()
})

test('带有消息的 skip() 方法', t => {
  // 如果测试包含其他逻辑,也请确保在此处返回。
  t.skip('this is skipped')
})

TODO 测试

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

js
// 使用了 todo 选项,但没有提供消息。
test('todo 选项', { todo: true }, t => {
  // 此代码会被执行,但不会被视为失败。
  throw new Error('这不会导致测试失败')
})

// 使用了 todo 选项,并提供了一条消息。
test('带有消息的 todo 选项', { todo: '这是一个 todo 测试' }, t => {
  // 此代码会被执行。
})

test('todo() 方法', t => {
  t.todo()
})

test('带有消息的 todo() 方法', t => {
  t.todo('这是一个 todo 测试,不会被视为失败')
  throw new Error('这不会导致测试失败')
})

describe()it() 的别名

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

js
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 模块导入。

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

only 测试

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

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

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

js
// 假设 Node.js 使用 --test-only 命令行选项运行。
// 套件的 'only' 选项已设置,因此这些测试将运行。
test('this test is run', { only: true }, async t => {
  // 在此测试中,默认情况下将运行所有子测试。
  await t.test('running subtest')

  // 测试上下文可以更新为使用 'only' 选项运行子测试。
  t.runOnly(true)
  await t.test('this subtest is now skipped')
  await t.test('this subtest is run', { only: true })

  // 将上下文切换回执行所有测试。
  t.runOnly(false)
  await t.test('this subtest is now run')

  // 明确地不运行这些测试。
  await t.test('skipped subtest 3', { only: false })
  await t.test('skipped subtest 4', { skip: true })
})

// 未设置 'only' 选项,因此此测试将被跳过。
test('this test is not run', () => {
  // 此代码未运行。
  throw new Error('fail')
})

describe('a suite', () => {
  // 已设置 'only' 选项,因此此测试将运行。
  it('this test is run', { only: true }, () => {
    // 此代码正在运行。
  })

  it('this test is not run', () => {
    // 此代码未运行。
    throw new Error('fail')
  })
})

describe.only('a suite', () => {
  // 已设置 'only' 选项,因此此测试将运行。
  it('this test is run', () => {
    // 此代码正在运行。
  })

  it('this test 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" 等)。

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

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

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

要使用模式匹配单个测试,可以在其前面加上所有祖先测试名称(用空格分隔),以确保其唯一性。例如,给定以下测试文件:

js
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> 在顶层报告为诊断警告。

js
test('一个创建异步活动的测试', t => {
  setImmediate(() => {
    t.test('创建太晚的子测试', t => {
      throw new Error('error1')
    })
  })

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

  // 测试在此行之后结束。
})

监听模式

新增于:v19.2.0, v18.13.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1 - 实验性

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

bash
node --test --watch

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

从命令行运行测试

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

bash
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}

当提供 --experimental-strip-types 时,将匹配以下附加模式:

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

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

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

匹配的文件将作为测试文件执行。有关测试文件执行的更多信息,请参阅 测试运行器执行模型 部分。

测试运行器执行模型

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

每个测试文件都被执行,就像它是一个常规脚本一样。也就是说,如果测试文件本身使用 node:test 来定义测试,那么所有这些测试都将在单个应用程序线程中执行,而不管 test()concurrency 选项的值是多少。

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

收集代码覆盖率

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1 - 实验性

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

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

js
/* node:coverage disable */
if (anAlwaysFalseCondition) {
  // 此分支中的代码永远不会执行,但这些行出于覆盖率目的而被忽略。
  // 所有在 'disable' 注释后的行都会被忽略,直到遇到相应的 'enable' 注释。
  console.log('this is never executed')
}
/* node:coverage enable */

也可以为指定行数禁用覆盖率。超过指定行数后,覆盖率将自动重新启用。如果没有显式提供行数,则忽略单行。

js
/* 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 文件,可以用作深入的代码覆盖率报告。

bash
node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info
  • 此报告器不会报告任何测试结果。
  • 此报告器最好与其他报告器一起使用。

模拟

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

js
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)

  // 重置全局跟踪的模拟。
  mock.reset()
})
js
'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)

  // 重置全局跟踪的模拟。
  mock.reset()
})

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

js
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 模块以及 Node.js 全局上下文中的 setTimeout 函数。

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

js
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()
})
js
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 属性中。通过测试上下文进行模拟的好处是,测试运行器将在测试完成后自动恢复所有模拟的定时器功能。

js
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)
})
js
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)
})

日期

模拟计时器 API 也允许模拟 Date 对象。这对于测试依赖时间的函数或模拟内部日历函数(如 Date.now())非常有用。

日期实现也是 MockTimers 类的一部分。请参考它以获取完整的 方法和功能列表。

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

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

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

test('mocks the Date object', context => {
  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['Date'] })
  // 如果未指定,则初始日期将基于 UNIX 纪元中的 0
  assert.strictEqual(Date.now(), 0)

  // 推进时间也会推进日期
  context.mock.timers.tick(9999)
  assert.strictEqual(Date.now(), 9999)
})
js
const assert = require('node:assert')
const { test } = require('node:test')

test('mocks the Date object', context => {
  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['Date'] })
  // 如果未指定,则初始日期将基于 UNIX 纪元中的 0
  assert.strictEqual(Date.now(), 0)

  // 推进时间也会推进日期
  context.mock.timers.tick(9999)
  assert.strictEqual(Date.now(), 9999)
})

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

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

test('mocks the Date object with initial time', context => {
  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['Date'], now: 100 })
  assert.strictEqual(Date.now(), 100)

  // 推进时间也会推进日期
  context.mock.timers.tick(200)
  assert.strictEqual(Date.now(), 300)
})
js
const assert = require('node:assert')
const { test } = require('node:test')

test('mocks the Date object with initial time', context => {
  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['Date'], now: 100 })
  assert.strictEqual(Date.now(), 100)

  // 推进时间也会推进日期
  context.mock.timers.tick(200)
  assert.strictEqual(Date.now(), 300)
})

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

**注意:**此方法将执行从新时间起过去的所有模拟计时器。

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

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

test('sets the time of a date object', context => {
  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['Date'], now: 100 })
  assert.strictEqual(Date.now(), 100)

  // 推进时间也会推进日期
  context.mock.timers.setTime(1000)
  context.mock.timers.tick(200)
  assert.strictEqual(Date.now(), 1200)
})
js
const assert = require('node:assert')
const { test } = require('node:test')

test('sets the time of a date object', context => {
  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['Date'], now: 100 })
  assert.strictEqual(Date.now(), 100)

  // 推进时间也会推进日期
  context.mock.timers.setTime(1000)
  context.mock.timers.tick(200)
  assert.strictEqual(Date.now(), 1200)
})

如果您有任何设置为在过去运行的计时器,它将被执行,就像调用了 .tick() 方法一样。如果您想测试已经过去的依赖时间的函数,这将非常有用。

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

test('runs timers as setTime passes ticks', context => {
  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] })
  const fn = context.mock.fn()
  setTimeout(fn, 1000)

  context.mock.timers.setTime(800)
  // 计时器未执行,因为时间尚未到达
  assert.strictEqual(fn.mock.callCount(), 0)
  assert.strictEqual(Date.now(), 800)

  context.mock.timers.setTime(1200)
  // 计时器已执行,因为时间现在已到达
  assert.strictEqual(fn.mock.callCount(), 1)
  assert.strictEqual(Date.now(), 1200)
})
js
const assert = require('node:assert')
const { test } = require('node:test')

test('runs timers as setTime passes ticks', context => {
  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] })
  const fn = context.mock.fn()
  setTimeout(fn, 1000)

  context.mock.timers.setTime(800)
  // 计时器未执行,因为时间尚未到达
  assert.strictEqual(fn.mock.callCount(), 0)
  assert.strictEqual(Date.now(), 800)

  context.mock.timers.setTime(1200)
  // 计时器已执行,因为时间现在已到达
  assert.strictEqual(fn.mock.callCount(), 1)
  assert.strictEqual(Date.now(), 1200)
})

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

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

test('runs timers as setTime passes ticks', context => {
  // 可选地选择要模拟的内容
  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()
  // 所有计时器都已执行,因为时间现在已到达
  assert.strictEqual(fn.mock.callCount(), 3)
  assert.strictEqual(Date.now(), 3000)
})
js
const assert = require('node:assert')
const { test } = require('node:test')

test('runs timers as setTime passes ticks', context => {
  // 可选地选择要模拟的内容
  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()
  // 所有计时器都已执行,因为时间现在已到达
  assert.strictEqual(fn.mock.callCount(), 3)
  assert.strictEqual(Date.now(), 3000)
})

快照测试

[稳定版: 1 - 实验版]

稳定版: 1 稳定性: 1.0 - 早期开发

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

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

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

js
// 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 的文件。快照文件的内容如下所示。每个快照都由测试的全名和一个计数器标识,以区分同一测试中的快照。

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

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

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

测试报告器

[历史]

版本变更
v19.9.0, v18.17.0报告器现在暴露在 node:test/reporters 中。
v19.6.0, v18.15.0新增于:v19.6.0, v18.15.0

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

以下内置报告器受支持:

  • spec spec 报告器以人类可读的格式输出测试结果。这是默认的报告器。
  • tap tap 报告器以 TAP 格式输出测试结果。
  • dot dot 报告器以紧凑的格式输出测试结果,其中每个通过的测试用 . 表示,每个失败的测试用 X 表示。
  • junit junit 报告器以 jUnit XML 格式输出测试结果。
  • lcov lcov 报告器在与 --experimental-test-coverage 标志一起使用时输出测试覆盖率。

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

报告器可通过 node:test/reporters 模块访问:

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

自定义 reporter

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

使用 <stream.Transform> 的自定义 reporter 示例:

js
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
js
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

使用生成器函数的自定义 reporter 示例:

js
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
      }
    }
  }
}
js
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

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

当只指定一个报告器时,目标位置默认为 stdout,除非显式提供目标位置。

run([options])

[历史]

版本变更
v23.0.0添加了 cwd 选项。
v23.0.0添加了代码覆盖率选项。
v22.8.0添加了 isolation 选项。
v22.6.0添加了 globPatterns 选项。
v22.0.0, v20.14.0添加了 forceExit 选项。
v20.1.0, v18.17.0添加了 testNamePatterns 选项。
v18.9.0, v16.19.0添加于:v18.9.0, v16.19.0
  • options <对象> 运行测试的配置选项。支持以下属性:

    • concurrency <数字> | <布尔值> 如果提供数字,则将并行运行多个测试进程,每个进程对应一个测试文件。如果为 true,则将并行运行 os.availableParallelism() - 1 个测试文件。如果为 false,则每次只运行一个测试文件。默认值: false

    • cwd: <字符串> 指定测试运行程序使用的当前工作目录。作为根据 测试运行程序执行模型 解析文件的基路径。默认值: process.cwd()

    • files: <数组> 包含要运行的文件列表的数组。默认值: 来自 测试运行程序执行模型 的匹配文件。

    • forceExit: <布尔值> 配置测试运行程序,即使事件循环仍处于活动状态,一旦所有已知测试都已完成执行,就退出进程。默认值: false

    • globPatterns: <数组> 包含用于匹配测试文件的 glob 模式列表的数组。此选项不能与 files 选项一起使用。默认值: 来自 测试运行程序执行模型 的匹配文件。

    • inspectPort <数字> | <函数> 设置测试子进程的检查器端口。这可以是一个数字,或者是一个不接受任何参数并返回数字的函数。如果提供空值,则每个进程都会获得自己的端口,从主进程的 process.debugPort 开始递增。如果 isolation 选项设置为 'none',则忽略此选项,因为不会生成子进程。默认值: undefined

    • isolation <字符串> 配置测试隔离的类型。如果设置为 'process',则每个测试文件都在单独的子进程中运行。如果设置为 'none',则所有测试文件都在当前进程中运行。默认值: 'process'

    • only: <布尔值> 如果为真值,则测试上下文将只运行设置了 only 选项的测试。

    • setup <函数> 一个接受 TestsStream 实例的函数,可在运行任何测试之前用于设置侦听器。默认值: undefined

    • execArgv <数组> 生成子进程时传递给 node 可执行文件的 CLI 标志数组。当 isolation'none' 时,此选项无效。默认值: []

    • argv <数组> 生成子进程时传递给每个测试文件的 CLI 标志数组。当 isolation'none' 时,此选项无效。默认值: []

    • signal <AbortSignal> 允许中止正在进行的测试执行。

    • testNamePatterns <字符串> | <正则表达式> | <数组> 字符串、正则表达式或正则表达式数组,可用于仅运行名称与提供的模式匹配的测试。测试名称模式被解释为 JavaScript 正则表达式。对于每个执行的测试,任何相应的测试钩子(例如 beforeEach())也会运行。默认值: undefined

    • testSkipPatterns <字符串> | <正则表达式> | <数组> 字符串、正则表达式或正则表达式数组,可用于排除运行名称与提供的模式匹配的测试。测试名称模式被解释为 JavaScript 正则表达式。对于每个执行的测试,任何相应的测试钩子(例如 beforeEach())也会运行。默认值: undefined

    • timeout <数字> 测试执行在失败之前的毫秒数。如果未指定,则子测试从其父测试继承此值。默认值: Infinity

    • watch <布尔值> 是否以监视模式运行。默认值: false

    • shard <对象> 在特定分片中运行测试。默认值: undefined

    • index <数字> 是介于 1 和 \<总数\> 之间的正整数,指定要运行的分片的索引。此选项是 必需的

    • total <数字> 是一个正整数,指定要将测试文件拆分成多少个分片。此选项是 必需的

    • coverage <布尔值> 启用 代码覆盖率 收集。默认值: false

    • coverageExcludeGlobs <字符串> | <数组> 使用 glob 模式排除代码覆盖率中的特定文件,该模式可以匹配绝对和相对文件路径。此属性仅在将 coverage 设置为 true 时适用。如果同时提供了 coverageExcludeGlobscoverageIncludeGlobs,则文件必须满足 两个 条件才能包含在覆盖率报告中。默认值: undefined

    • coverageIncludeGlobs <字符串> | <数组> 使用 glob 模式包含代码覆盖率中的特定文件,该模式可以匹配绝对和相对文件路径。此属性仅在将 coverage 设置为 true 时适用。如果同时提供了 coverageExcludeGlobscoverageIncludeGlobs,则文件必须满足 两个 条件才能包含在覆盖率报告中。默认值: undefined

    • lineCoverage <数字> 要求覆盖行的最小百分比。如果代码覆盖率未达到指定的阈值,则进程将以代码 1 退出。默认值: 0

    • branchCoverage <数字> 要求覆盖分支的最小百分比。如果代码覆盖率未达到指定的阈值,则进程将以代码 1 退出。默认值: 0

    • functionCoverage <数字> 要求覆盖函数的最小百分比。如果代码覆盖率未达到指定的阈值,则进程将以代码 1 退出。默认值: 0

  • 返回值:<TestsStream>

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

js
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)
js
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])

新增于: v22.0.0, v20.13.0

  • name <字符串> 套件的名称,在报告测试结果时显示。默认值: fnname 属性,如果 fn 没有名称则为 '<anonymous>'
  • options <对象> 套件的可选配置选项。这支持与 test([name][, options][, fn]) 相同的选项。
  • fn <函数> | <异步函数> 声明嵌套测试和套件的套件函数。此函数的第一个参数是 SuiteContext 对象。默认值: 一个空操作函数。
  • 返回值: <Promise> 立即 fulfilled 并返回 undefined

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

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

新增于: v22.0.0, v20.13.0

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

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

新增于: v22.0.0, v20.13.0

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

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

新增于: v22.0.0, v20.13.0

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

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

[历史]

版本变更
v20.2.0, v18.17.0添加了 skiptodoonly 简写。
v18.8.0, v16.18.0添加了 signal 选项。
v18.7.0, v16.17.0添加了 timeout 选项。
v18.0.0, v16.17.0新增于: v18.0.0, v16.17.0
  • name <字符串> 测试的名称,在报告测试结果时显示。默认值: fnname 属性,如果 fn 没有名称则为 '<anonymous>'

  • options <对象> 测试的配置选项。支持以下属性:

    • concurrency <数字> | <布尔值> 如果提供一个数字,则应用程序线程中将并行运行这么多测试。如果为 true,则所有计划的异步测试都将在线程内并发运行。如果为 false,则一次只运行一个测试。如果未指定,则子测试将继承其父级的此值。默认值: false
    • only <布尔值> 如果为真值,并且测试上下文配置为运行 only 测试,则将运行此测试。否则,测试将被跳过。默认值: false
    • signal <AbortSignal> 允许中止正在进行的测试。
    • skip <布尔值> | <字符串> 如果为真值,则跳过测试。如果提供字符串,则该字符串在测试结果中显示为跳过测试的原因。默认值: false
    • todo <布尔值> | <字符串> 如果为真值,则测试标记为 TODO。如果提供字符串,则该字符串在测试结果中显示为测试为 TODO 的原因。默认值: false
    • timeout <数字> 测试将在多少毫秒后失败。如果未指定,则子测试将继承其父级的此值。默认值: Infinity
    • plan <数字> 预期在测试中运行的断言和子测试的数量。如果测试中运行的断言数量与计划中指定的数量不匹配,则测试将失败。默认值: undefined
  • fn <函数> | <异步函数> 被测函数。此函数的第一个参数是 TestContext 对象。如果测试使用回调,则回调函数作为第二个参数传递。默认值: 一个空操作函数。

  • 返回值: <Promise> 测试完成后,使用 undefined 填充,如果测试在套件内运行,则立即填充。对于顶级测试,返回值通常可以丢弃。但是,应该使用子测试的返回值来防止父测试先完成并取消子测试,如下例所示。

js
test('顶级测试', async t => {
  // 下面的子测试中的 setTimeout() 将导致它在下一行删除“await”后比其父测试存活更久。一旦父测试完成,它将取消任何未完成的子测试。
  await t.test('运行时间较长的子测试', async t => {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, 1000)
    })
  })
})

可以使用 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()](/zh/api/test#suitename-options-fn) 的别名。

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])

新增于:v19.8.0, v18.15.0

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

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

[历史]

版本变更
v19.8.0, v18.16.0调用 it() 现在等同于调用 test()
v18.6.0, v16.17.0新增于:v18.6.0, v16.17.0

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])

新增于:v19.8.0, v18.15.0

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

before([fn][, options])

新增于: v18.8.0, v16.18.0

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

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

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

after([fn][, options])

新增于: v18.8.0, v16.18.0

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

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

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

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

beforeEach([fn][, options])

新增于:v18.8.0, v16.18.0

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

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

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

afterEach([fn][, options])

新增于: v18.8.0, v16.18.0

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

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

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

snapshot

新增于:v22.3.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1.0 - 早期开发

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

snapshot.setDefaultSnapshotSerializers(serializers)

新增于:v22.3.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1.0 - 早期开发

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

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

snapshot.setResolveSnapshotPath(fn)

新增于: v22.3.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.0 - 早期开发

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

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

类: MockFunctionContext

新增于: v19.1.0, v18.13.0

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

ctx.calls

新增于: v19.1.0, v18.13.0

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

  • arguments <数组> 传递给模拟函数的参数数组。
  • error <任意值> 如果模拟函数抛出异常,则此属性包含抛出的值。默认值: undefined
  • result <任意值> 模拟函数返回的值。
  • stack <错误> 一个 Error 对象,其堆栈可用于确定模拟函数调用的调用位置。
  • target <函数> | <未定义> 如果模拟函数是构造函数,则此字段包含正在构造的类。否则为 undefined
  • this <任意值> 模拟函数的 this 值。

ctx.callCount()

新增于: v19.1.0, v18.13.0

  • 返回值: <整数> 此模拟函数已被调用的次数。

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

ctx.mockImplementation(implementation)

新增于: v19.1.0, v18.13.0

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

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

js
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])

新增于:v19.1.0, v18.13.0

  • implementation <函数> | <异步函数> 将用作模拟在 onCall 指定的调用次数的实现的函数。
  • onCall <整数> 将使用 implementation 的调用次数。如果指定的调用已经发生,则抛出异常。**默认值:**下一次调用的次数。

此函数用于更改现有模拟的单个调用的行为。一旦调用 onCall,模拟将恢复到如果没有调用 mockImplementationOnce() 将使用的任何行为。

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

js
test('更改模拟行为一次', 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()

新增于: v19.3.0, v18.13.0

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

ctx.restore()

新增于: v19.1.0, v18.13.0

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

类: MockModuleContext

新增于: v22.3.0, v20.18.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.0 - 早期开发

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

ctx.restore()

新增于: v22.3.0, v20.18.0

重置模拟模块的实现。

类: MockTracker

新增于: v19.1.0, v18.13.0

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

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

新增于:v19.1.0, v18.13.0

  • original <Function> | <AsyncFunction> 一个可选函数,用于创建模拟对象。**默认值:**一个空操作函数。

  • implementation <Function> | <AsyncFunction> 一个可选函数,用作 original 的模拟实现。这对于创建模拟对象很有用,这些模拟对象对指定次数的调用表现出一种行为,然后恢复 original 的行为。**默认值:**由 original 指定的函数。

  • options <Object> 模拟函数的可选配置选项。支持以下属性:

    • times <integer> 模拟对象将使用 implementation 行为的次数。模拟函数被调用 times 次后,它将自动恢复 original 的行为。此值必须大于零的整数。默认值:Infinity
  • 返回值:<Proxy> 模拟函数。模拟函数包含一个特殊的 mock 属性,它是 MockFunctionContext 的实例,可用于检查和更改模拟函数的行为。

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

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

js
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])

新增于:v19.3.0, v18.13.0

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

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

新增于:v19.1.0, v18.13.0

  • 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 的实例,可用于检查和更改模拟方法的行为。

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

js
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])

新增于: v22.3.0, v20.18.0

[稳定性: 1 - 实验性]

稳定性: 1 稳定性: 1.0 - 早期开发

  • specifier <字符串> | <URL> 用于标识要模拟的模块的字符串。

  • options <对象> 模拟模块的可选配置选项。支持以下属性:

    • cache <布尔值> 如果为 false,则每次调用 require()import() 都会生成一个新的模拟模块。如果为 true,则后续调用将返回相同的模块模拟,并且模拟模块将插入到 CommonJS 缓存中。默认值: false。
    • defaultExport <任意值> 用作模拟模块默认导出的可选值。如果未提供此值,则 ESM 模拟不包含默认导出。如果模拟是 CommonJS 或内置模块,则此设置用作 module.exports 的值。如果未提供此值,则 CJS 和内置模拟使用空对象作为 module.exports 的值。
    • namedExports <对象> 一个可选对象,其键和值用于创建模拟模块的命名导出。如果模拟是 CommonJS 或内置模块,则这些值将复制到 module.exports。因此,如果使用命名导出和非对象默认导出创建模拟,则该模拟在用作 CJS 或内置模块时将引发异常。
  • 返回值: <MockModuleContext> 可用于操作模拟的对象。

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

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

js
test('mocks a builtin module in both module systems', async t => {
  // 创建 'node:readline' 的模拟,其中包含名为 'fn' 的命名导出,
  // 该导出在原始 'node:readline' 模块中不存在。
  const mock = t.mock.module('node:readline', {
    namedExports: {
      fn() {
        return 42
      },
    },
  })

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

  // cursorTo() 是原始 'node:readline' 模块的导出。
  assert.strictEqual(esmImpl.cursorTo, undefined)
  assert.strictEqual(cjsImpl.cursorTo, undefined)
  assert.strictEqual(esmImpl.fn(), 42)
  assert.strictEqual(cjsImpl.fn(), 42)

  mock.restore()

  // 模拟已恢复,因此返回原始内置模块。
  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()

新增于: v19.1.0, v18.13.0

此函数恢复之前由此 MockTracker 创建的所有模拟的默认行为,并取消模拟与 MockTracker 实例的关联。取消关联后,仍然可以使用模拟,但 MockTracker 实例不能再用于重置其行为或以其他方式与它们交互。

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

mock.restoreAll()

新增于: v19.1.0, v18.13.0

此函数恢复之前由此 MockTracker 创建的所有模拟的默认行为。与 mock.reset() 不同,mock.restoreAll() 不会取消模拟与 MockTracker 实例的关联。

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

新增于: v19.3.0, v18.13.0

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

类: MockTimers

[历史]

版本变更
v23.1.0模拟计时器现已稳定。
v20.4.0, v18.19.0新增于:v20.4.0, v18.19.0

[稳定性: 2 - 稳定]

稳定性: 2 稳定性: 2 - 稳定

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

MockTimers 也能够模拟 Date 对象。

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

timers.enable([enableOptions])

[历史]

版本变更
v21.2.0, v20.11.0更新参数为一个包含可用 API 和默认初始纪元时间选项的对象。
v20.4.0, v18.19.0新增于:v20.4.0, v18.19.0

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

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

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

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

无初始时间的示例用法:

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

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

设置初始时间的示例用法:

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

使用初始 Date 对象作为时间设置的示例用法:

js
import { mock } from 'node:test'
mock.timers.enable({ apis: ['Date'], now: new Date() })
js
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()

新增于:v20.4.0, v18.19.0

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

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

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

timers[Symbol.dispose]()

调用timers.reset()

timers.tick([milliseconds])

新增于:v20.4.0, v18.19.0

为所有模拟的计时器推进时间。

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

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

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

js
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)

  // 推进时间
  context.mock.timers.tick(9999)

  assert.strictEqual(fn.mock.callCount(), 1)
})
js
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)

  // 推进时间
  context.mock.timers.tick(9999)

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

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

js
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)
})
js
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)
})

使用.tick推进时间也会推进在启用模拟后创建的任何Date对象的时间(如果也设置了Date被模拟)。

js
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)

  // 推进时间
  context.mock.timers.tick(9999)
  assert.strictEqual(fn.mock.callCount(), 1)
  assert.strictEqual(Date.now(), 9999)
})
js
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)

  // 推进时间
  context.mock.timers.tick(9999)
  assert.strictEqual(fn.mock.callCount(), 1)
  assert.strictEqual(Date.now(), 9999)
})

使用清除函数

如前所述,计时器的所有清除函数(clearTimeoutclearIntervalclearImmediate)都被隐式模拟。让我们来看一个使用 setTimeout 的例子:

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

test('模拟 setTimeout 使其同步执行,无需实际等待', context => {
  const fn = context.mock.fn()

  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['setTimeout'] })
  const id = setTimeout(fn, 9999)

  // 也被隐式模拟
  clearTimeout(id)
  context.mock.timers.tick(9999)

  // 由于 setTimeout 已被清除,模拟函数将永远不会被调用
  assert.strictEqual(fn.mock.callCount(), 0)
})
js
const assert = require('node:assert')
const { test } = require('node:test')

test('模拟 setTimeout 使其同步执行,无需实际等待', context => {
  const fn = context.mock.fn()

  // 可选地选择要模拟的内容
  context.mock.timers.enable({ apis: ['setTimeout'] })
  const id = setTimeout(fn, 9999)

  // 也被隐式模拟
  clearTimeout(id)
  context.mock.timers.tick(9999)

  // 由于 setTimeout 已被清除,模拟函数将永远不会被调用
  assert.strictEqual(fn.mock.callCount(), 0)
})

使用 Node.js 定时器模块

启用定时器模拟后,node:timersnode:timers/promises 模块以及来自 Node.js 全局上下文的定时器将被启用:

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

js
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)
})
js
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 支持:

js
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)
  }
})
js
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()

新增于:v20.4.0, v18.19.0

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

下面的示例立即触发所有挂起的计时器,导致它们立即执行,无需任何延迟。

js
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)

  // 注意,如果两个计时器具有相同的超时时间,
  // 执行顺序是有保证的
  setTimeout(() => results.push(3), 8888)
  setTimeout(() => results.push(2), 8888)

  assert.deepStrictEqual(results, [])

  context.mock.timers.runAll()
  assert.deepStrictEqual(results, [3, 2, 1])
  // Date 对象也被推进到最远计时器的时间
  assert.strictEqual(Date.now(), 9999)
})
js
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)

  // 注意,如果两个计时器具有相同的超时时间,
  // 执行顺序是有保证的
  setTimeout(() => results.push(3), 8888)
  setTimeout(() => results.push(2), 8888)

  assert.deepStrictEqual(results, [])

  context.mock.timers.runAll()
  assert.deepStrictEqual(results, [3, 2, 1])
  // Date 对象也被推进到最远计时器的时间
  assert.strictEqual(Date.now(), 9999)
})

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

timers.setTime(milliseconds)

新增于:v21.2.0, v20.11.0

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

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

test('runAll 函数按照给定的顺序执行', context => {
  const now = Date.now()
  const setTime = 1000
  // Date.now 未被模拟
  assert.deepStrictEqual(Date.now(), now)

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

test('setTime 替换当前时间', context => {
  const now = Date.now()
  const setTime = 1000
  // Date.now 未被模拟
  assert.deepStrictEqual(Date.now(), now)

  context.mock.timers.enable({ apis: ['Date'] })
  context.mock.timers.setTime(setTime)
  // Date.now 现在是 1000
  assert.strictEqual(Date.now(), setTime)
})

日期和计时器协同工作

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

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

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

test('runAll 函数按照给定顺序执行', 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, [])
  // 日期被推进,但计时器没有计时
  assert.strictEqual(Date.now(), 12000)
})
js
const assert = require('node:assert')
const { test } = require('node:test')

test('runAll 函数按照给定顺序执行', 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, [])
  // 日期被推进,但计时器没有计时
  assert.strictEqual(Date.now(), 12000)
})

类: TestsStream

[历史]

版本变更
v20.0.0, v19.9.0, v18.17.0为测试套件添加了 test:passtest:fail 事件的类型。
v18.9.0, v16.19.0新增于: v18.9.0, v16.19.0

成功调用 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> 表示行号及其覆盖次数的行数组。

    • line <number> 行号。

    • count <number> 行覆盖次数。

    • thresholds <Object> 包含每种覆盖类型是否达到阈值的对象。

    • function <number> 函数覆盖率阈值。

    • branch <number> 分支覆盖率阈值。

    • line <number> 代码覆盖率阈值。

    • 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'

发出测试出队事件,紧接在测试执行之前。此事件的发出顺序不保证与测试定义的顺序相同。对应的声明顺序事件是 'test:start'

事件: 'test:diagnostic'

调用 context.diagnostic 时发出。此事件保证按照测试定义的顺序发出。

事件:'test:enqueue'

当一个测试被排队等待执行时发出。

事件:'test:fail'

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

事件:'test:pass'

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

事件: 'test:plan'

  • data <对象>
    • column <数字> | <未定义> 测试定义所在的列号,如果测试是通过 REPL 运行的,则为 undefined
    • file <字符串> | <未定义> 测试文件的路径,如果测试是通过 REPL 运行的,则为 undefined
    • line <数字> | <未定义> 测试定义的行号,如果测试是通过 REPL 运行的,则为 undefined
    • nesting <数字> 测试的嵌套级别。
    • count <数字> 已运行的子测试数量。

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

事件:'test:start'

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

事件: 'test:stderr'

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

事件: 'test:stdout'

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

事件: 'test:summary'

  • data <对象>

    • counts <对象> 包含各种测试结果计数的对象。

    • cancelled <数字> 已取消测试的总数。

    • failed <数字> 测试失败的总数。

    • passed <数字> 测试通过的总数。

    • skipped <数字> 已跳过测试的总数。

    • suites <数字> 已运行的套件总数。

    • tests <数字> 已运行的测试总数,不包括套件。

    • todo <数字> TODO 测试的总数。

    • topLevel <数字> 顶级测试和套件的总数。

    • duration_ms <数字> 测试运行的持续时间(毫秒)。

    • file <字符串> | <未定义> 生成摘要的测试文件的路径。如果摘要对应于多个文件,则此值为 undefined

    • success <布尔值> 指示测试运行是否成功。如果发生任何错误情况,例如测试失败或未满足覆盖率阈值,则此值将设置为 false

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

事件:'test:watch:drained'

当监视模式下不再有测试排队等待执行时发出。

类:TestContext

[历史]

版本变更
v20.1.0, v18.17.0添加了 before 函数到 TestContext
v18.0.0, v16.17.0新增于:v18.0.0, v16.17.0

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

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

新增于:v20.1.0, v18.17.0

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

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

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

新增于:v18.8.0, v16.18.0

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

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

js
test('顶级测试', async t => {
  t.beforeEach(t => t.diagnostic(`即将运行 ${t.name}`))
  await t.test('这是一个子测试', t => {
    assert.ok('此处一些相关的断言')
  })
})

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

新增于: v19.3.0, v18.13.0

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

此函数用于创建在当前测试完成后运行的钩子。

js
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])

新增于: v18.8.0, v16.18.0

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

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

js
test('顶层测试', async t => {
  t.afterEach(t => t.diagnostic(`已完成运行 ${t.name}`))
  await t.test('这是一个子测试', t => {
    assert.ok('此处为一些相关的断言')
  })
})

context.assert

新增于:v22.2.0, v20.15.0

一个包含绑定到 context 的断言方法的对象。此处公开来自 node:assert 模块的顶级函数,用于创建测试计划。

js
test('test', t => {
  t.plan(1)
  t.assert.strictEqual(true, true)
})

context.assert.snapshot(value[, options])

新增于:v22.3.0

[稳定性:1 - 实验性]

稳定性:1 稳定性:1.0 - 早期开发

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

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

js
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)

新增于:v18.0.0, v16.17.0

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

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

context.filePath

新增于:v22.6.0, v20.16.0

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

context.fullName

新增于:v22.3.0

测试及其所有祖先的名称,以 > 分隔。

context.name

新增于:v18.8.0, v16.18.0

测试的名称。

context.plan(count)

[历史]

版本变更
v23.4.0此函数不再是实验性的。
v22.2.0, v20.15.0新增于:v22.2.0, v20.15.0
  • count <number> 预期运行的断言和子测试的数量。

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

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

在处理异步代码时,可以使用 plan 函数来确保运行了正确的断言数量:

js
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()
  })
})

context.runOnly(shouldRunOnlyTests)

新增于:v18.0.0, v16.17.0

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

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

js
test('顶层测试', t => {
  // 测试上下文可以设置为运行带有 'only' 选项的子测试。
  t.runOnly(true)
  return Promise.all([t.test('此子测试现在被跳过'), t.test('此子测试正在运行', { only: true })])
})

context.signal

新增于:v18.7.0, v16.17.0

当测试被中止时,可用于中止测试子任务。

js
test('顶层测试', async t => {
  await fetch('some/uri', { signal: t.signal })
})

context.skip([message])

新增于:v18.0.0, v16.17.0

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

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

js
test('top level test', t => {
  // 即使测试包含其他逻辑,也务必在此处返回。
  t.skip('this is skipped')
})

context.todo([message])

新增于:v18.0.0, v16.17.0

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

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

js
test('top level test', t => {
  // 此测试被标记为 `TODO`
  t.todo('this is a todo')
})

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

[历史]

版本变更
v18.8.0, v16.18.0添加 signal 选项。
v18.7.0, v16.17.0添加 timeout 选项。
v18.0.0, v16.17.0新增于:v18.0.0, v16.17.0
  • name <字符串> 子测试的名称,在报告测试结果时显示。默认值:fnname 属性,如果 fn 没有名称则为 '<anonymous>'

  • options <对象> 子测试的配置选项。支持以下属性:

    • concurrency <数字> | <布尔值> | <空值> 如果提供数字,则应用程序线程中将并行运行这么多测试。如果为 true,则将并行运行所有子测试。如果为 false,则每次只运行一个测试。如果未指定,子测试将从其父级继承此值。默认值:null
    • only <布尔值> 如果为真值,并且测试上下文配置为运行 only 测试,则将运行此测试。否则,测试将被跳过。默认值:false
    • signal <AbortSignal> 允许中止正在进行的测试。
    • skip <布尔值> | <字符串> 如果为真值,则跳过测试。如果提供字符串,则该字符串将在测试结果中显示为跳过测试的原因。默认值:false
    • todo <布尔值> | <字符串> 如果为真值,则测试标记为 TODO。如果提供字符串,则该字符串将在测试结果中显示为测试为 TODO 的原因。默认值:false
    • timeout <数字> 测试将在多少毫秒后失败。如果未指定,子测试将从其父级继承此值。默认值:Infinity
    • plan <数字> 预期在测试中运行的断言和子测试的数量。如果测试中运行的断言数量与计划中指定的数量不匹配,则测试将失败。默认值:undefined
  • fn <函数> | <异步函数> 被测函数。此函数的第一个参数是 TestContext 对象。如果测试使用回调,则回调函数作为第二个参数传递。**默认值:**一个空操作函数。

  • 返回值: <Promise> 测试完成后,以 undefined 作为已完成状态。

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

js
test('顶级测试', async t => {
  await t.test('这是一个子测试', { only: false, skip: false, concurrency: 1, todo: false, plan: 1 }, t => {
    t.assert.ok('此处有一些相关的断言')
  })
})

类: SuiteContext

新增于: v18.7.0, v16.17.0

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

context.filePath

新增于: v22.6.0

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

context.name

新增于: v18.8.0, v16.18.0

套件的名称。

context.signal

新增于: v18.7.0, v16.17.0

当测试被中止时,可用于中止测试子任务。