Erlang/OTP EUnit 版本演进全解:从 2.0 到 2.11 的核心特性、宏与源码实现
2026/9/23 13:59:54 网站建设 项目流程

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 等源码剖析randomDelayscale_timeoutscapturedOutput、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/1byte_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触发延迟,延迟时长在MinSecMaxSec秒之间均匀随机取值(源码内部先将秒转换为毫秒再调用timer:sleep/1)。使用时形如?randomDelay(0.5, 0, 1),表示约 50% 的概率在 0~1 秒内随机休眠。注意两点:

  • 宏被包裹在fun() -> ... end立即调用中,符合 EUnit 宏“不泄漏局部变量”的惯例(见 eunit.hrl 顶部注释);
  • 与调试类宏一致,如果定义了NODEBUGrandomDelay会被替换为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/1byte_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 返回值实例化进测试;
  • Wherelocal|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.2make release只把必要代码放入发布目录,文档与示例不再混入;make release_docs将文档放入发布树doc目录;make release_tests将测试独立成目录。SBOM 方面,为asmjitzlib增加optional_components_of关系,将makeerts下的 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 各版本变更,可以总结出以下对当前开发者仍然适用的实践准则:

  1. 测试命名..._test()是简单测试函数,..._test_()是测试生成器;m_tests模块会被自动纳入eunit:test(m)的执行范围,若需精确控制可用exact_execution选项;
  2. 断言优先用专用宏assertMatch/assertEqual/assertException家族比裸模式匹配产生更详细的错误信息;判断异常用assertError/assertExit/assertThrow
  3. 捕获输出做断言:用?capturedOutput验证标准输出内容;
  4. 时序敏感代码用randomDelay:在并发路径上注入随机延迟以暴露竞态;
  5. 慢主机上缩放超时:使用{scale_timeouts, N}而非逐个修改超时;
  6. fixture 的Where慎用local:超时被杀后 cleanup 不会执行,持久化资源应依赖默认的spawn
  7. 生成大量测试用惰性生成器:每次调用返回单条测试加新生成器,避免一次性占用过多内存(模式见 chapter.md 的 Lazy generators 一节);
  8. 保证可移植编译:若需在无 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询