Erlang/OTP EUnit 版本演进全解:从 2.0 到 2.11 的核心特性、宏与源码实现
【免费下载链接】otpErlang/OTP项目地址: https://gitcode.com/gh_mirrors/ot/otp
EUnit 是 Erlang/OTP 官方的轻量级单元测试框架(首次随 OTP 发布的版本即由 Richard Carlsson 贡献),本文基于仓库内的官方发布说明 lib/eunit/doc/notes.md 完整梳理其从 2.0 到 2.11 的演进历程,并结合 eunit.hrl、eunit.erl 等源码剖析randomDelay、scale_timeouts、capturedOutput、surefire 报告、peer 节点等关键机制的实际实现。读完本文,你将能准确理解每个 EUnit 版本新增了什么、为什么这样设计,以及如何在 OTP 当前版本中正确使用这些特性编写健壮的单元测试。
EUnit 版本演进总览
EUnit 于 2.0 版本正式进入 Erlang/OTP,此后经历了十余个版本的迭代。当前仓库中 vsn.mk 标注的版本号为EUNIT_VSN = 2.11。以下总览表概括了每个版本的核心变更(依据 lib/eunit/doc/notes.md):
| 版本 | 类型 | 核心变更 |
|---|---|---|
| 2.0 | 新功能 | 首个随 OTP 发布的 EUnit 版本(作者 Richard Carlsson) |
| 2.1 | 新功能 | 重构事件协议,修复超时导致挂起的问题;新增 surefire 报告后端(面向 Maven/Bamboo) |
| 2.1.5~2.1.7 | 新功能/修复 | 文档构建可开源化;surefire 错误信息深度提升至 100;修复 surefire 停止时的竞态 |
| 2.2.1 | 修复 | 每个测试套件生成独立的 surefire XML;新增assertNotMatch/assertNotEqual/assertNotException宏;debugMsg打印当前进程 pid;Module_tests.erl不再被执行两次 |
| 2.2.2 | 新功能 | OTP 支持并行 make(如make -j6) |
| 2.2.3 | 新功能 | 新增no_tty选项静默默认终端报告;测试表示{test,M,F}正式化({M,F}二元组被弃用);识别 R15 新栈轨迹格式;生成器返回异常值会被启发式检测并告警;surefire 输出使用 UTF-8 编码 |
| 2.2.4~2.2.13 | 修复/新功能 | 编码注释、宏包裹 begin...end、I/O 协议错误处理、appup 修正、Unicode 安全、debugVal/2截断深度控制 |
| 2.3.1 | 修复 | 断言移入独立头文件后测试自动启用失效的问题 |
| 2.3.3~2.3.8 | 修复 | surefire 报告名去除内嵌双引号、Unicode 原子显示、get_until显式编码处理 |
| 2.4~2.4.1 | 新功能 | 移除编译器警告;surefire 剥离控制码避免生成非法 XML |
| 2.5 | 新功能 | surefire 跳过非法 XML 1.0 字符;新增?capturedOutput宏;新增异常打印深度限制选项 |
| 2.6~2.6.1 | 新功能/修复 | 修复编译器警告;eunit_surefire报告处理器自动创建存储 XML 所需的目录 |
| 2.7 | 新功能 | 测试用例超时时,报告中包含 stacktrace |
| 2.8 | 新功能 | 可选项关闭对_tests后缀关联模块的自动执行(避免源与测试同目录时的重复执行) |
| 2.8.1 | 修复 | exact_execution选项与 application 原语配合工作 |
| 2.8.2 | 新功能 | 用tuple_size/1、byte_size/1替换size/1(后者未被 JIT 优化且影响 Dialyzer 类型推断) |
| 2.9 | 新功能 | 新增scale_timeouts选项,可按比例缩放超时 |
| 2.9.1 | 新功能 | 文档迁移至 Markdown 与 ExDoc |
| 2.10 | 新功能 | 实现 EEP-69 名义类型(Nominal Types),重构 opaque 处理;许可证头统一为 SPDX 格式 |
| 2.10.1 | 新功能 | 移除已弃用的 slave 模块用法;fixture 的节点启动参数接受字符串或字符串列表 |
| 2.10.2 | 新功能 | release 目录结构梳理;SBOM(软件物料清单)改进 |
| 2.10.3 | 修复 | 修复{node,...}实例化(传递节点名而非 pid),恢复非分布式节点的net_kernel自动启动 |
| 2.11 | 新功能 | 新增randomDelay宏;支持-unsafe属性与不安全函数调用告警 |
EUnit 2.11:randomDelay宏与-unsafe支持
作为当前最新版本,EUnit 2.11 带来两项重要能力,均可在仓库源码中找到对应实现。
randomDelay:注入随机延迟,暴露时序竞态
发布说明指出,2.11 新增了randomDelay宏,用于在测试执行中引入非确定性延迟,帮助暴露**竞态条件(race condition)**与依赖精确时序才能显现的缺陷。在 eunit.hrl 中可以看到它的完整实现:
-ifdef(NODEBUG). -define(randomDelay(Prob, MinSec, MaxSec), ok). -else. -define(randomDelay(Prob, MinSec, MaxSec), (fun() -> case rand:uniform() < (Prob) of true -> %% Convert seconds to milliseconds for timer:sleep Delay = (MinSec) + rand:uniform() * ((MaxSec) - (MinSec)), timer:sleep(round(Delay * 1000)); false -> ok end end)()). -endif.其语义是:以概率Prob触发延迟,延迟时长在MinSec到MaxSec秒之间均匀随机取值(源码内部先将秒转换为毫秒再调用timer:sleep/1)。使用时形如?randomDelay(0.5, 0, 1),表示约 50% 的概率在 0~1 秒内随机休眠。注意两点:
- 宏被包裹在
fun() -> ... end立即调用中,符合 EUnit 宏“不泄漏局部变量”的惯例(见 eunit.hrl 顶部注释); - 与调试类宏一致,如果定义了
NODEBUG,randomDelay会被替换为ok,不产生任何运行时效果。
该宏的官方文档说明位于 chapter.md 的“Macros for instrumentation”一节。
-unsafe属性与不安全函数调用检测
2.11 的另一项能力与编译器联动:支持-unsafe属性,用于把函数标记为“不安全使用”。它类似于弃用(deprecation)但又彼此独立,且编译器默认会对调用 OTP 中已知始终不安全的函数生成警告。配套地,xref新增了三类分析:跨应用调用缺少-doc属性的函数(undocumented_function_calls)、调用标记为-doc false.的函数(private_function_calls)、以及调用不安全函数(unsafe_function_calls)。这为依赖审计与 API 治理提供了静态检查手段。
超时、并行与测试控制选项的演进
超时与并行执行是 EUnit 的核心控制能力,多个版本持续打磨。
scale_timeouts:按比例缩放超时(2.9)
EUnit 2.9 引入了scale_timeouts选项,允许按数值因子整体缩放所有超时,适合在较慢的宿主机上运行测试。在 eunit.erl 的test/2选项文档中明确说明:{scale_timeouts,10}使超时放大 10 倍,{scale_timeouts,0.1}则缩短为原来的 1/10。仓库自测套件 eunit_SUITE.erl 中即包含scale_timeouts_test/1对该行为的回归验证。
timeout 原语的底层实现
超时是测试表示中的一等公民:{timeout, Time::number(), Tests}(Time 单位为秒,例如0.1表示 1/10 秒)。在解析层,eunit_data.erl 将其转换为内部时钟刻度:
parse({timeout, N, T}, Options) when is_number(N), N >= 0 -> group(#group{tests = T, options = Options, timeout = round(N * ?TICKS_PER_SECOND)});同时 eunit_data.erl 的push_timeout/3展示了关键语义:当超时设置在带 fixture(context)的分组外层时,超时时间包含 setup 与 cleanup 的执行时间,且一旦超时触发,整个 fixture 会被强制终止(不执行 cleanup)。单个测试的默认超时为 5 秒。EUnit 2.7 起,测试超时时报告中会附带 stacktrace,便于定位卡住的代码位置。
并行执行与取消报告(2.10)
EUnit 2.10 修复了一个并行测试场景的缺陷:当多个测试并行运行、其中一个因 setup 失败被取消时,该测试现在会被正确报告为 “cancelled”,此前该取消会被静默忽略。回归测试report_failed_setup_inparallel_test/1记录在 eunit_SUITE.erl 中。并行控制原语包括{inparallel, Tests}(尽可能并行)与{inparallel, N, Tests}(最多 N 个并发子测试)。
exact_execution:精确控制测试范围(2.8/2.8.1)
默认情况下,eunit:test(m)不仅测试模块m,还会自动寻找并执行m_tests模块中的测试。当源码与测试模块位于同一目录、执行顺序敏感时,这种自动扩展可能造成重复执行。EUnit 2.8 增加了开关exact_execution(布尔标志,置true时不再自动执行_tests后缀模块),2.8.1 又修复了该选项与{application, ...}原语配合时失效的问题。在 eunit.erl 的选项文档中对此有明确描述,exact_execution的语义验证见 eunit_SUITE.erl 的eunit_exact_test/1。
测试表示与断言宏的演进
测试表示正式化:{test,M,F}(2.2.3)
EUnit 2.2.3 引入正式的三元组表示{test,M,F},指代M:F/0函数,同时原有的二元组{M,F}被标记为弃用。从 chapter.md 的 “Simple test objects” 一节可以看到完整定义:一个简单测试对象可以是零参 fun、{test, Module, Function}元组、带行号的{LineNumber, SimpleTest}对(通常由?_test(...)宏产生)。此外,?前缀宏(如?_assert(...))本质是?_test(?assert(...))的语法糖,而_test(Expr)展开为{?LINE, fun () -> (Expr) end},自动携带源码行号。
断言宏家族扩充(2.2.1)
EUnit 2.2.1 新增了assertNotMatch(Guard, Expr)、assertNotEqual(Unexpected, Expr)与assertNotException(Class, Term, Expr),补全了断言宏的正反两面;这些宏在下划线前缀形式(?_assertNotMatch等)下均可用,实现位于 eunit.hrl。注意 2.2.3 还改进了错误信息布局,先打印 stacktrace 再打印错误项。
?capturedOutput:断言标准输出(2.5)
EUnit 会捕获测试函数写入标准输出的内容,2.5 新增的?capturedOutput宏允许在测试内部取回这些输出并进行断言。其实现(见 eunit.hrl)在?UNDER_EUNIT为真时调用eunit_proc:get_output(),否则返回空字符串:
-define(capturedOutput, case ?UNDER_EUNIT of true -> eunit_proc:get_output(); false -> "" end).典型用法:
io:format("Hello~n"), ?assertEqual("Hello\n", ?capturedOutput)debugVal/2与打印深度控制(2.3/2.5)
- 2.3 新增
debugVal/2,允许显式指定项的打印截断深度;默认深度由EUNIT_DEBUG_VAL_DEPTH宏控制(默认 15,见 eunit.hrl),?debugVal(Expr)会打印形如f(X) = 42的源码与值对; - 2.5 增加了限制测试套件异常打印深度的选项(对应
print_depth选项,见 eunit.erl)。
性能相关的小改进(2.8.2)
EUnit 2.8.2 将内部size/1调用替换为tuple_size/1或byte_size/1,因为size/1不受 JIT 优化,且可能使 Dialyzer 推导出更差的类型。这既是一次内部清理,也提示了 Erlang 编程的通用最佳实践:已知是元组用tuple_size/1,已知是二进制用byte_size/1(若可能为 bitstring,需先用is_binary/1确认)。
surefire 报告后端:面向 Maven/Bamboo 的持续打磨
surefire 是 EUnit 的事件监听器后端,用于生成 Maven 与 Atlassian Bamboo 可解析的 XML 测试报告,入口见 eunit_surefire.erl。它经历了一条完整的打磨路线:
| 版本 | 变更 |
|---|---|
| 2.1 | 新增 surefire 报告后端 |
| 2.1.7 | 报告内错误消息深度提升到 100(终端输出才有深度限制,XML 应包含更多信息);修复停止时向 eunit 回传导致竞态的问题 |
| 2.2.1 | 每个测试套件生成独立 XML(此前所有模块挤在一个报告里,归属随机);修正报告输出为 UTF-8 |
| 2.3.3 | 报告名不再内嵌双引号 |
| 2.4.1 | 从 surefire 输出中剥离控制码,避免生成非法 XML |
| 2.5 | 跳过非法 XML 1.0 字符 |
| 2.6.1 | 自动创建存储 XML 所需的目录(回归测试surefire_ensure_dir_test/1在 eunit_SUITE.erl 中) |
使用示例(见 eunit_surefire.erl 的模块文档):
eunit:test([fib, eunit_examples], [{report,{eunit_surefire,[{dir,"."}]}}]).报告目录通过dir选项配置,默认值为"."(源码中-define(XMLDIR, "."))。
节点与 fixture:从 slave 到 peer 的演进
{node,...}实例化修复(2.10.3)
EUnit 2.10.3 修复了{node, ...}实例化时的一个缺陷:现在正确地传递**节点名(node name)**而非 pid,同时恢复了非分布式节点的net_kernel自动启动。这意味着基于 peer 节点的测试在普通(非分布式)节点上也能可靠工作。
移除 slave,参数化 Args(2.10.1)
EUnit 2.10.1 移除了对已弃用slave模块的全部使用(slave已被peer取代)。同时,fixture 中启动测试节点的Args参数现在既接受字符串也接受字符串列表:传字符串时会被解析为参数列表,单/双引号包裹的文本视为单个参数并去掉引号,可用反斜杠转义保留引号(详见 chapter.md 的 Fixtures 一节)。
fixture 的四种形态
EUnit 的 fixture 表示在 chapter.md 中有系统定义,核心类型为:
Setup:() -> R,测试前执行并返回状态值;Cleanup:(R) -> any(),测试结束后执行(无论成败、超时与否);Instantiator:(R) -> Tests或{with, [AbstractTestFun]},把 setup 返回值实例化进测试;Where:local|spawn|{spawn, Node},控制执行位置。
四种组合形式分别为{setup, Setup, Tests}、{setup, Setup, Cleanup, Tests}、{setup, Where, Setup, Tests}与{setup, Where, Setup, Cleanup, Tests};foreach为每个测试集重复 setup/cleanup;foreachx额外携带每项独立的X参数。Where默认是spawn(当前进程负责 setup/teardown,测试在子进程执行);local模式下若测试超时导致进程被杀,cleanup 不会执行,因此对文件等持久化 fixture 应避免使用local。
并行运行、事件协议与内部机制(2.1/2.2.5/2.10)
EUnit 2.1 重构了事件协议:修复了可能导致 EUnit 挂起的超时问题,并让编写新的报告后端变得更加容易;同时测试表示不再被遍历两次(第一次仅用于枚举),消除了对生成器写法的一些限制。从 eunit.erl 的listeners/1可以看到监听器机制:eunit_tty始终运行(负责发送最终{result,...}消息),其余通过{report, {Mod, Opts}}选项挂载,event_log选项则可把原始事件写入日志文件便于调试。
其他值得一提的内部改进包括:2.2.5 将 EUnit 宏包裹进begin ... end块(避免宏展开带来的作用域问题);2.10 移除了宏内对已弃用正则库的依赖并改用re;2.2.10 使 EUnit 应用 Unicode 安全。
构建、发布与合规基础设施的演进
近年来 EUnit 的版本变更越来越聚焦于工程基础设施:
- 2.9.1:文档迁移到 Markdown 与 ExDoc(本仓库中的 lib/eunit/doc/guides/chapter.md 即为该迁移的产物);
- 2.10.2:
make release只把必要代码放入发布目录,文档与示例不再混入;make release_docs将文档放入发布树doc目录;make release_tests将测试独立成目录。SBOM 方面,为asmjit、zlib增加optional_components_of关系,将make与erts下的 autoconf 脚本归类为build_tool_of,所有configure/Makefile.in等文件纳入特定 SPDX 包。发布后可执行./Install -minimal \pwd`并将 release 加入PATH` 来验证; - 2.10:统一许可证头格式,加入
SPDX-License-Identifier(如 eunit.hrl 头部的Apache-2.0 OR LGPL-2.1-or-later); - 2.10:作为 EEP-69 名义类型的附带影响,重构了所有 opaque 类型处理逻辑并改进 Dialyzer 的 opaque 警告——opaque 类型检查的向后兼容性不保留,旧警告措辞可能略有变化,新增
opaque_union警告及no_opaque_union关闭选项。
从发布历史中提炼的 EUnit 使用要点
综合 notes.md 各版本变更,可以总结出以下对当前开发者仍然适用的实践准则:
- 测试命名:
..._test()是简单测试函数,..._test_()是测试生成器;m_tests模块会被自动纳入eunit:test(m)的执行范围,若需精确控制可用exact_execution选项; - 断言优先用专用宏:
assertMatch/assertEqual/assertException家族比裸模式匹配产生更详细的错误信息;判断异常用assertError/assertExit/assertThrow; - 捕获输出做断言:用
?capturedOutput验证标准输出内容; - 时序敏感代码用
randomDelay:在并发路径上注入随机延迟以暴露竞态; - 慢主机上缩放超时:使用
{scale_timeouts, N}而非逐个修改超时; - fixture 的
Where慎用local:超时被杀后 cleanup 不会执行,持久化资源应依赖默认的spawn; - 生成大量测试用惰性生成器:每次调用返回单条测试加新生成器,避免一次性占用过多内存(模式见 chapter.md 的 Lazy generators 一节);
- 保证可移植编译:若需在无 EUnit 环境下编译,将
-include_lib("eunit/include/eunit.hrl")与测试代码置于-ifdef(TEST)条件编译块内,或默认定义NOTEST并在需要时用-DTEST覆盖。
EUnit 的演进史既是一部测试框架功能完善史,也折射出 Erlang/OTP 整体工程实践的变迁——从替代正则库、移除弃用模块,到统一 SPDX 许可证头与 SBOM 合规。理解这些变更背后的源码实现(宏展开逻辑、超时解析、监听器机制),能帮助你在编写测试时更精准地选择 API,并预判其行为边界。
【免费下载链接】otpErlang/OTP项目地址: https://gitcode.com/gh_mirrors/ot/otp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考