最近做了一个 SSE 接口 mock 工具 sse-stuntman 的时候用到 Node.js 内置的单元测试 runner 和覆盖率。故有此文。
下面是 Node.js 官方关于可支持报告格式的说明:
支持以下内置报告器:
spec:spec报告器以易读的人性化格式输出测试结果,为默认报告器。tap:tap报告器以 TAP 格式输出测试结果。dot:dot报告器采用极简紧凑格式输出测试结果:测试通过显示.,测试失败显示X。junit:junit 报告器以 jUnit XML 格式输出测试结果。lcov:搭配--experimental-test-coverage参数使用时,lcov报告器可输出测试覆盖率数据。
本文主要是看看各种格式长什么样,以便大家选择。
1. --test-reporter=spec #默认
md
ℹ tests 78
ℹ suites 29
ℹ pass 78
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3257.1436
ℹ start of coverage report
ℹ -----------------------------------------------------------------------------------------------------
ℹ file | line % | branch % | funcs % | uncovered lines
ℹ -----------------------------------------------------------------------------------------------------
ℹ src | | | |
ℹ commands | | | |
ℹ create-scenario.mjs | 73.60 | 85.71 | 75.00 | 86-87 95-125
ℹ config-loader.mjs | 93.92 | 64.71 | 100.00 | 102-106 142-143 145-146
ℹ openai-stream.mjs | 91.07 | 95.45 | 100.00 | 51-70
ℹ utils | | | |
ℹ providers | | | |
ℹ anthropic | | | |
ℹ event-generator.mjs | 99.10 | 83.33 | 88.89 | 90
ℹ stream.mjs | 96.02 | 90.91 | 100.00 | 179-182 184-187
ℹ server.mjs | 100.00 | 93.75 | 100.00 |
ℹ string.mjs | 100.00 | 100.00 | 100.00 |
ℹ token.mjs | 100.00 | 100.00 | 100.00 |
ℹ -----------------------------------------------------------------------------------------------------
ℹ all files | 95.80 | 91.31 | 95.60 |
ℹ -----------------------------------------------------------------------------------------------------
ℹ end of coverage report
2. --test-reporter=tap
bash
ok 7 - anthropic-stream
---
duration_ms: 136.9186
type: 'suite'
...
1..7
# tests 79
# suites 31
# pass 79
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 3501.0162
# start of coverage report
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
# file | line % | branch % | funcs % | uncovered lines
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
# src | | | |
# commands | | | |
# create-scenario.mjs | 73.60 | 85.71 | 75.00 | 86-87 95-125
# config-loader.mjs | 93.92 | 64.71 | 100.00 | 102-106 142-143 145-146
# openai-stream.mjs | 90.95 | 88.00 | 100.00 | 54-74
# utils | | | |
# providers | | | |
# anthropic | | | |
# event-generator.mjs | 99.10 | 83.33 | 88.89 | 90
# stream.mjs | 96.15 | 84.00 | 100.00 | 186-189 191-194
# server.mjs | 100.00 | 95.65 | 85.71 |
# string.mjs | 100.00 | 100.00 | 100.00 |
# token.mjs | 100.00 | 100.00 | 100.00 |
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------# all files | 87.95 | 73.93 | 85.56 |
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------# end of coverage report
3. --test-reporter=dot
css
❯ node --test --experimental-test-coverage --test-coverage-exclude="src/**/*.test.mjs" --test-reporter=dot
因为我的测试文件都是通过的,故都输出绿色点:
..............................................................................................................
4. --test-reporter=junit
sh
❯ node --test --experimental-test-coverage --test-coverage-exclude="src/**/*.test.mjs" --test-reporter=junit
xml
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite name="anthropic-stream" time="0.125695" disabled="0" errors="0" tests="3" failures="0" skipped="0" hostname="PC-LEGEND80S">
<testsuite name="writeAnthropicStream()" time="0.123685" disabled="0" errors="0" tests="8" failures="0" skipped="0" hostname="PC-LEGEND80S">
<testcase name="should emit message_start event first" time="0.003565" classname="test"/>
<testcase name="should honor delay multiplier" time="0.117796" classname="test"/>
<testcase name="should use custom model name" time="0.000357" classname="test"/>
</testsuite>
<testsuite name="writeAnthropicErrorStream()" time="0.000812" disabled="0" errors="0" tests="3" failures="0" skipped="0" hostname="PC-LEGEND80S">
<testcase name="should emit error SSE event for rate-limit" time="0.000196" classname="test"/>
<testcase name="should emit error SSE event for content-filter" time="0.000335" classname="test"/>
<testcase name="should emit error SSE event for server-error" time="0.000163" classname="test"/>
</testsuite>
<testsuite name="writeAnthropicNonStreamingResponse()" time="0.000735" disabled="0" errors="0" tests="3" failures="0" skipped="0" hostname="PC-LEGEND80S">
<testcase name="should return JSON with Anthropic Messages API format" time="0.000283" classname="test"/>
<testcase name="should include usage statistics" time="0.000204" classname="test"/>
<testcase name="should contain valid message id" time="0.000143" classname="test"/>
</testsuite>
</testsuite>
<!-- tests 79 -->
<!-- suites 31 -->
<!-- pass 79 -->
<!-- fail 0 -->
<!-- cancelled 0 -->
<!-- skipped 0 -->
<!-- todo 0 -->
<!-- duration_ms 3529.7627 -->
</testsuites>
5. --test-reporter=lcov
sh
❯ node --test --experimental-test-coverage --test-coverage-exclude="src/**/*.test.mjs" --test-reporter=lcov
ts
SF:src\utils\token.mjs
FN:22,calculateTokens
FN:25,anonymous_1
FNDA:41,calculateTokens
FNDA:2666,anonymous_1
FNF:2
FNH:2
BRDA:1,0,0,3
BRDA:22,1,0,41
BRDA:25,2,0,2666
BRDA:25,3,0,1041
BRF:4
BRH:4
DA:1,3
DA:2,3
<...省略>
DA:25,41
DA:26,41
DA:27,41
DA:28,41
DA:29,41
DA:30,41
DA:31,41
LH:31
LF:31
end_of_record
6. 自定义
我想输出成 json 格式,这样方便 git diff,那就需要自定义 reporter 了。
把官方示例改一下就行,先看输出效果:
js
❯ node --test --experimental-test-coverage --test-coverage-exclude="src/**/*.test.mjs" --test-reporter ./scripts/json-reporter.mjs src/scenario-parser.test.mjs
ts
tests 13
suites 4
pass 13
fail 0
cancelled 0
skipped 0
todo 0
duration_ms 269.0277
{
"totalLineCount": 731,
"totalBranchCount": 41,
"totalFunctionCount": 20,
"coveredLineCount": 438,
"coveredBranchCount": 32,
"coveredFunctionCount": 10,
"coveredLinePercent": 59.91792065663475,
"coveredBranchPercent": 78.04878048780488,
"coveredFunctionPercent": 50
}
官方示例 nodejs.org/docs/latest... 修改如下:
js
// scripts/json-reporter.mjs
export default async function* customReporter(source) {
for await (const event of source) {
switch (event.type) {
case "test:watch:drained":
yield "test watch queue drained\n"
break
case "test:watch:restarted":
yield "test watch restarted due to file change\n"
break
case "test:fail":
yield `"${event.data.name}" 🔴\n`
break
case "test:diagnostic":
case "test:stderr":
case "test:stdout":
yield `${event.data.message}\n`
break
case "test:coverage": {
yield `\n${JSON.stringify(event.data.summary.totals, null, 2)}\n`
break
}
}
}
}
共有 9 种 event.type,这些事件类型反映了 Node.js 内置测试框架的生命周期:
test:enqueue--- 测试入队(等待执行)test:dequeue--- 测试出队(开始执行)test:start--- 测试开始运行test:pass--- 测试通过test:complete--- 测试完成test:plan--- 测试计划(确定要运行的测试集)test:diagnostic--- 诊断信息(如 tests/suites/pass/fail 等统计)test:summary--- 测试摘要test:coverage--- 覆盖率报告