Hardhat 3 的 node:test 测试报告器:@nomicfoundation/hardhat-node-test-reporter 功能解析与演进全解
【免费下载链接】hardhatHardhat is a development environment to compile, deploy, test, and debug your Ethereum software.项目地址: https://gitcode.com/GitHub_Trending/ha/hardhat
@nomicfoundation/hardhat-node-test-reporter是 Hardhat 3 中负责node:test测试输出渲染的内部组件,被hardhat-node-test-runner插件直接使用。本篇文章以该包 CHANGELOG.md 为骨架,结合其 README.md 与src/下的完整源码实现,系统讲解它的安装使用、输出风格、全部自定义特性,以及从 3.0.0 到 3.1.1 的每一次版本演进背后的技术细节,帮助你理解 Hardhat 3 的测试报告是如何“一行一行”生成的。
一、它是什么:一个内置的 node:test 自定义报告器
hardhat-node-test-reporter是 Hardhat 3 的node:test报告器。它并不是一个让用户自行组合的独立测试框架,而是跟随hardhat-node-test-runner插件内置分发:安装 Hardhat 3 的node:test测试插件后,报告器会自动被使用,无需单独安装。
从包元数据(package.json)可以看出它的定位:
- 包名:
@nomicfoundation/hardhat-node-test-reporter,当前版本 3.1.1,MIT 协议,ESM("type": "module"); - 依赖非常精简,只有两个运行时依赖:
@actions/core(用于 GitHub Actions 注解)与jest-diff(用于期望值/实际值 diff); - 入口导出为
./dist/src/reporter.js,同时导出./package.json供消费者读取包清单。
也就是说,这个包本质上是一个 Node.js 官方node:test/reporters规范下的自定义报告器实现,只是针对 Hardhat 生态做了大量定制。
二、安装与两种使用方式
2.1 随插件内置,开箱即用
按照 README 的说明,使用 Hardhat 3 的node:test插件时报告器会自动启用,绝大多数用户不需要单独安装。
2.2 独立安装
如果你要在自己的项目里直接使用这个报告器,可以通过 npm 安装(可选--save-dev):
npm install --save-dev @nomicfoundation/hardhat-node-test-reporter2.3 用 node 直接运行
如果你不想依赖 Hardhat,也可以把它作为普通 Node 报告器直接交给node的--test-reporter参数:
node --test --test-reporter=@nomicfoundation/hardhat-node-test-reporter这是最轻量的接入方式:任何使用node --test的项目都能立刻获得与 Hardhat 3 完全一致的测试输出体验。
三、报告器的设计哲学与输出结构
从 reporter.ts 的源码注释中,我们可以读到这个报告器的三条核心设计原则:
- 尽力模拟 Mocha 默认的
Spec报告器,让 Hardhat 老用户感到熟悉; - 尽快输出信息,且按测试“定义顺序”输出(
node:test上报的事件顺序即定义顺序,可能与实际执行顺序不同); - 整体输出分三个阶段:
- 测试执行过程中:逐条打印通过/失败/跳过等信息;
- 测试运行结束后:基于
node:test发出的全局诊断信息输出汇总; - 最后:统一输出所有失败测试的失败原因详情。
源码中还特别说明:报告器把“格式化”和“打印”彻底分离——格式化模块(formatting.ts)只负责把事件转成字符串、且从不以换行结尾,换行统一由生成器(generator)控制。这让输出布局的调整变得非常可控。
四、核心特性详解(基于 README 与源码双重印证)
4.1 慢测试标记
慢测试阈值为75ms。当一个测试用例耗时超过该值时,会在输出末尾追加红色斜体的耗时提示。该常量在源码中定义:
export const SLOW_TEST_THRESHOLD = 75;对应 reporter.ts,其输出格式化函数位于 formatting.ts:
export function formatSlowTestInfo(durationMs: number): string { return ` ${styleText(["red", "italic"], `(${Math.floor(durationMs)}ms)`)}`; }4.2 测试覆盖率
该报告器目前不支持测试覆盖率。当收到test:coverage事件时,会直接打印红色提示Test coverage not supported by this reporter(见 reporter.ts)。
4.3 GitHub Actions 注解
报告器原生适配 GitHub Actions:默认情况下,失败的测试会自动生成 error annotations(错误注解,显示在 GitHub PR 的文件 diff 上)。可以通过环境变量关闭:
NO_GITHUB_ACTIONS_ANNOTATIONS=true该逻辑在 github-actions.ts 中实现:
- 仅当
process.env.GITHUB_ACTIONS存在且未设置NO_GITHUB_ACTIONS_ANNOTATIONS时才生效(L16-L21); - 通过动态
import("@actions/core")延迟加载@actions/core,本地运行时不加载该依赖(L10-L11); - 注解会携带
file、startLine、startColumn、title信息;只有错误位置落在GITHUB_WORKSPACE(或当前工作目录)内的失败测试才会生成注解,且文件路径会转换为相对路径(L42-L70)。
4.4 颜色输出与图例
报告器在支持的终端里默认输出彩色文本,颜色能力继承自 Node.js 内置的util.styleText。可用环境变量强制开关:
# 强制关闭颜色 FORCE_COLOR=0 # 强制开启颜色 FORCE_COLOR=1node命令行也支持--no-color/--color参数。颜色图例如下:
| 输出类型 | 颜色 |
|---|---|
| Cancelled(取消) | Gray(灰) |
| Error(错误) | Red(红) |
| Failure(失败) | Red(红) |
| Skipped(跳过) | Cyan(青) |
| Success(成功) | Green 加对勾(绿) |
| TODO | Blue(蓝) |
这些颜色映射在 formatting.ts 中有完整对应:跳过用青色- name,TODO 用蓝色+ name,成功用绿色✔ name,失败用红色N) name。
4.5 嵌套缩进
套件(suite)的嵌套层级通过每层 2 个空格的缩进体现,缩进量由嵌套深度计算:
function nestingToIndentationLength(nesting: number): number { return (nesting + 1) * 2; }见 formatting.ts。顶层测试之间还会用空行分隔。
4.6 错误格式化
这是报告器最核心的能力之一。它会以人类可读的方式格式化错误,具体行为包括:
- 尝试打印错误对象及其堆栈;
- 如果错误带有
expected/actual属性(可 diff),打印期望值与实际值的 diff(基于jest-diff); - 打印聚合错误(
AggregateError)的内部错误; - 截断错误 cause 链:默认最多打印 10 层 cause,CI 环境下为 100 层(对应 CHANGELOG 3.0.1 的修复);
- 从堆栈中隐藏 Node 内部帧(包括测试运行器内部帧);
- 把
file://形式的 URL 转换为相对路径(在 Windows 上也有效)。
上述逻辑全部实现在 error-formatting.ts 中,关键常量如下:
const MAX_ERROR_CHAIN_LENGTH = isCi() ? 100 : 10;同时,错误消息中的[ERR_ASSERTION]、[ERR_TEST_FAILURE]前缀会被剔除,断言失败会输出AssertionError: The expression evaluated to a falsy value等精简信息,并尽力移除消息中冗余的 diff 内容(L137-L175)。
4.7 诊断信息聚合
node:test会在运行期间发出test:diagnostic事件。报告器会收集全部诊断信息,在运行结束时统一输出;对于无法识别或无法解析的诊断消息,则在已知诊断之后原样打印(未识别消息会附加ℹ蓝色信息符号)。
“已知诊断”(well-known diagnostics)包括:
tests(测试总数)suites(套件数)pass(通过数)fail(失败数)cancelled(取消数)skipped(跳过数)todo(待办数)duration_ms(总耗时)
解析逻辑见 diagnostics.ts:只解析nesting === 0的顶层诊断,按名字 数值的格式拆分,无法拆分的进入unusedDiagnostics列表。最终汇总输出的样式为2 passing (12ms),并分别用红色、青色、蓝色、灰色展示 failing / skipped / todo / cancelled 统计(formatting.ts)。
五、从 CHANGELOG 看版本演进:3.0.0 → 3.1.1
CHANGELOG 记录了 10 次发布。逐条对照源码,我们可以还原每一条变更背后的技术动机。
5.1 3.0.0 —— Hardhat 3 首发
29cc141: First release of Hardhat 3!该包伴随 Hardhat 3 首次发布,作为node:test报告器的基础组件出现,其核心骨架(事件驱动生成器 + 格式化模块分离)从第一天就已定型。
5.2 3.0.1 —— 修复 cause 链被截断的问题
ef714f7: Fix test error cause chains being cut off. The default is now 10 causes (up from 3). In CI environments, it's 100.
在 error-formatting.ts 中可以看到对应实现:
const MAX_ERROR_CHAIN_LENGTH = isCi() ? 100 : 10;当错误链长度超过上限时,会用占位错误替换超出的部分:
cause = new Error( `The error chain has been truncated because it's too long (limit: ${MAX_ERROR_CHAIN_LENGTH})`, );CI 环境检测逻辑isCi()支持 GitHub Actions、Vercel Now、AWS CodeBuild、Travis、CircleCI、GitLab CI、Jenkins、TeamCity 等常见 CI 平台(见 ci.ts)。
5.3 3.0.2 —— 多个测试任务运行时的汇总合并
7697451: Test summaries are now merged when running multiple test tasks (issue #7053)
在 Hardhat 3 中,test任务可能被拆分/并行运行多次。此时需要区分“由父任务统一汇总”与“自己直接输出汇总”两种情况。对应的实现是HardhatTestReporterConfig.testSummaryIndex:
export interface HardhatTestReporterConfig { testOnlyMessage?: string; testSummaryIndex: number; }见 reporter.ts。当testSummaryIndex === 0(表示任务被直接运行)时,报告器自行打印汇总与失败原因;否则报告器以结构化对象形式把{ failed, passed, skipped, todo, failureOutput }yield 给上层任务,由父任务合并后再统一输出(L324-L352)。
5.4 3.0.3 与 3.0.4 —— 工程化调整
23c0d36: Optimize imports(3.0.3):优化导入;7fb721b: [chore] Move to packages/ folder(3.0.4):将包迁移到 monorepo 的packages/目录,即当前路径 packages/hardhat-node-test-reporter。
这两条是纯工程性变更,不影响对外行为。
5.5 3.0.5 —— 等待所有返回的 promise
d16d82a: Await all returned promises for better debuggability
为了更好的可调试性,报告器会await所有返回的 promise。对应的是annotatePR的异步调用(await annotatePR(event.data),见 reporter.ts),确保 GitHub Actions 注解在失败原因打印前已经完成写入。
5.6 3.0.6 —— 用 util.styleText 替换 chalk
79205cc: Replace chalk with chalk → util.styleText
这是一次很有意思的依赖瘦身:报告器的全部着色能力改用 Node.js 内置的node:util的styleText实现,从而移除了chalk依赖。当前源码中所有颜色都通过styleText("red", ...)、styleText("gray", ...)、styleText("cyan", ...)、styleText("blue", ...)、styleText(["red", "italic"], ...)完成(见 formatting.ts),这也正是 README 中颜色行为“继承自 Node.js styleText”的由来。
5.7 3.0.7 —— 导出 package.json
8452f97: Export ./package.json so consumers can import the package's manifest
在 package.json 的exports字段中可以看到:
"exports": { ".": "./dist/src/reporter.js", "./package.json": "./package.json" }这样消费者可以直接读取包清单(例如获取版本号做运行时判断)。
5.8 3.1.0 —— Solidity 错误栈追踪特例
9d5b96c: Add a special case to the error formatting when a SolidityError is found, so that Solidity stack traces are more prominent.
这是 Hardhat 生态最贴合的一条特性。当测试失败的错误是(或 cause 链中包含)EDR 模拟网络抛出的SolidityError时:
- 报告器会沿着 cause 链查找
SolidityError(其name === "SolidityError"且带solidityStack字段); - 找到后,以Solidity stack trace:红色标题 + 灰色缩进堆栈的形式,把 Solidity 栈单独突出显示,并且不再打印包含它的整个 cause 链,避免干扰;
- 该逻辑同时兼容 viem 场景——viem 会把 Solidity 异常包在
ContractFunctionExecutionError的 cause 链中,同样会被识别出来。
对应实现见 error-formatting.ts。这意味着在 Hardhat 3 中,合约测试失败时你能一眼看到最关键的 Solidity 回滚栈,而不是被几十层 JS 堆栈淹没。
5.9 3.1.1 —— 依赖升级
9c54c73: Update @actions/core and jest-diff to their latest major versions.
将@actions/core与jest-diff升级到最新大版本,二者分别支撑 GitHub Actions 注解与断言 diff 功能(见 package.json)。
六、源码级原理:报告器如何工作
6.1 事件驱动的异步生成器
报告器本体是一个接收TestEventSource(AsyncGenerator<TestEvent>)的异步生成器函数(reporter.ts)。它维护:
stack:当前正在执行的测试/套件栈,用于还原测试上下文(嵌套 describe);lastPrintedIndex:记录最后一个已打印的栈元素下标,避免重复打印共享的 describe 上下文;diagnostics:收集所有test:diagnostic事件;preFormattedFailureReasons:预格式化的失败原因数组——失败发生时立即格式化并存起来,结尾统一打印,避免在内存中保留大对象。
核心循环对test:start/test:pass/test:fail/test:stderr/test:stdout/test:coverage等事件做分支处理。特别地,顶层文件级通过事件会被静默跳过(isTopLevelFilePassEvent判断nesting === 0且line === 1、column === 1,见 node-test-utils.ts),因为文件本身通过并不值得展示。
6.2 失败原因的判型逻辑
node-test-error-utils.ts 负责区分三类典型的node:test失败:
subtestsFailed:套件仅因子测试失败而失败,不再重复打印套件级失败(交给子测试自身);cancelledByParent:测试因父级失败被取消,只以灰色形式简短列出,不打印详情;testCodeFailure(且exitCode !== 0):整个测试文件执行失败,输出Test file execution failed (exit code N)。
cleanupTestFailError还会把ERR_TEST_FAILURE包裹层剥掉,直接暴露真正 cause 错误。
6.3 与 hardhat-node-test-runner 的集成
报告器并不是孤立组件。task-action.ts 直接导入hardhatTestReporter:
import { hardhatTestReporter } from "@nomicfoundation/hardhat-node-test-reporter";Hardhat 3 的test任务调用node:test的run(),通过.compose()把事件流接入报告器,再用pipeline把输出写入 stdout(task-action.ts)。报告器 yield 出的结构化对象({ failed, passed, skipped, todo, failureOutput })会被任务层捕获并作为TestRunResult返回,供上层判断测试通过与否。此外,任务层还会在测试前自动执行build编译(可用--no-compile关闭),并设置HH_TEST=true、NODE_ENV=test环境变量。
七、总结
@nomicfoundation/hardhat-node-test-reporter虽然定位为 Hardhat 3 的内部组件,却是一个把node:test事件流处理做到极致的范例:以异步生成器保持低延迟输出、以格式化/打印分离保证布局可控、以util.styleText零依赖着色、以 cause 链深度控制与SolidityError特例照顾区块链测试场景、以 GitHub Actions 注解打通 CI 反馈闭环。
从 CHANGELOG 的演进脉络看,这个包的每次发布都对应源码中一个可验证的具体改进——依赖瘦身(3.0.6)、错误链修复(3.0.1)、汇总合并(3.0.2)、Solidity 栈突出(3.1.0)——如果你想深入研读实现细节,推荐按 reporter.ts → formatting.ts → error-formatting.ts → diagnostics.ts → github-actions.ts 的顺序阅读,即可完整掌握一个生产级 Node.js 自定义测试报告器的全部实现技巧。
上图为该报告器的实际运行演示(来自包内 README.md 引用的 demo.gif),展示了通过/失败/跳过等状态的彩色输出样式。
【免费下载链接】hardhatHardhat is a development environment to compile, deploy, test, and debug your Ethereum software.项目地址: https://gitcode.com/GitHub_Trending/ha/hardhat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考