当前位置: 首页 > news >正文

一行use解决:Hammox.Protect宏如何让Elixir测试模块自带契约检查

一行use解决:Hammox.Protect宏如何让Elixir测试模块自带契约检查

【免费下载链接】hammox🏝 automated contract testing via type checking for Elixir functions and mocks项目地址: https://gitcode.com/gh_mirrors/ha/hammox

还在Elixir测试里手写setup_all、再一个个从测试上下文里掏匿名函数吗?Elixir契约测试库Hammox给出了更简单的答案:只需在测试模块顶部加一行use Hammox.Protect,它就自动为你被测模块的每个回调函数生成"带契约检查"的版本——调用时自动按 behaviour 的 typespec 声明校验参数与返回值,违规立即抛出清晰的Hammox.TypeMatchError

Hammox.Protect 三步快速上手:一行 use 开启契约检查

对新手来说,理解"契约检查"只要一句话:你的函数必须遵守 behaviour 里用@callback写下的"合同",参数类型、返回类型都不许错。Hammox.Protect 就是把这份合同变成运行时自动执行的法律。

第 1 步:添加依赖

mix.exs中声明依赖(仅测试环境使用):

def deps do [{:hammox, "~> 0.7", only: :test}] end

第 2 步:在测试模块加一行 use

假设RealDatabase实现了Database这个 behaviour:

defmodule RealDatabaseTest do use ExUnit.Case, async: true use Hammox.Protect, module: RealDatabase, behaviour: Database test "get_users/0 返回值符合契约" do assert {:ok, ["real-joe"]} == get_users() end end

第 3 步:像普通函数一样调用

注意测试里直接写get_users(),而不是RealDatabase.get_users()。效果类似import,但每个函数都被自动"包裹"了一层类型检查。

🎯 就这么多。不需要 setup、不需要匿名函数、不需要测试上下文传递。

Protect 宏的工作原理:自动收集回调并注入类型校验

这一行的魔法藏在哪?宏的实现位于lib/hammox/protect.ex,流程非常直白:

  1. 解析选项:从:module:behaviour:funs中确定要保护哪些函数
  2. 收集回调:通过Code.Typespec.fetch_callbacks/1读取 behaviour 的全部@callback声明
  3. 生成本地函数:为每个回调在你的测试模块中定义一个同名函数,内部先调用原函数,再用 Hammox 的类型引擎(lib/hammox/type_engine.ex)校验参数和返回值
  4. 违规即抛出:类型不匹配时抛出Hammox.TypeMatchError(定义见lib/hammox/type_match_error.ex

由于检查发生在运行时,那些 Dialyzer 静态分析够不着的 mock 数据、边界返回值都能被抓住——这正是 Hammox 相比静态类型检查的核心价值。

Hammox.Protect 三个选项速查:module、behaviour 与 funs

选项必填说明
:module✅ 是要实现契约的实现模块(通常就是被测模块)
:behaviour可选定义契约的 behaviour;省略时默认用:module本身声明的回调
:funs可选明确列出要保护的函数,如[foo: 0, bar: 1]

当回调和实现写在同一个模块时(Elixir 常见写法),只需:module一个选项:

defmodule Calculator do @callback add(integer(), integer()) :: integer() def add(a, b), do: a + b end # 测试里只写一行: use Hammox.Protect, module: Calculator

一个实现模块对应多个 behaviour时,可以写多组behaviour/funs。注意:每组:funs只作用于紧跟在它前面的那个:behaviour,省略:funs则保护该 behaviour 的全部回调:

use Hammox.Protect, module: MyApp.Service, behaviour: MyApp.Cache, funs: [get: 1], # 只保护 Cache 的 get/1 behaviour: MyApp.Logger # 未给 funs,保护 Logger 全部回调

官方测试用例中的真实用法可以参考test/hammox/protect_test.exs,覆盖了单模块、多 behaviour 等全部场景。

契约被打破时:Hammox.TypeMatchError 长什么样

当函数返回值不符合@callback声明时,测试会立刻失败,报错信息直指问题核心:

** (Hammox.TypeMatchError) Returned value ["joe", "jim"] does not match type {:ok, [binary()]} | {:error, term()}.

典型的翻车场景:behaviour 升级为返回{:ok, ...}元组后,旧的 mock 或旧实现仍返回裸值——测试表面通过、生产必炸。有了契约检查,这类问题在测试阶段就无处遁形。

Hammox.Protect 与显式 Hammox.protect 对比:怎么选?

Hammox 本身(lib/hammox.ex)提供了显式的Hammox.protect/3系列 API,返回匿名函数,配合setup_all使用。两者对比一目了然:

维度use Hammox.Protect(宏)Hammox.protect/3(显式)
代码量一行,最简洁需 setup + 上下文传参
调用方式get_users()直接调用get_users_0.()匿名函数
灵活度编译期确定,全局生效运行时确定,可按测试定制
适合场景测试整个 behaviour 的常规实现只保护个别函数、动态组合

经验法则:日常测试优先用宏,需要精细控制(比如某条测试只想部分保护)时再退回显式 API。Hammox 对 Mox 完全兼容,两者可以混用。

常见坑与报错信息速查

⚠️ 编译期就可能遇到的两个报错,都来自Hammox.Protect.extract_opts!/1的参数校验:

  • 忘记:modulePlease specify :module to protect with Hammox.Protect.
  • 模块没有任何回调(普通模块或空的 behaviour)→The module X does not contain any callbacks. Please use a behaviour with at least one callback.

另外两个运行时的注意点:

  1. 函数必须有 typespec:behaviour 中找不到对应@callback时会抛出TypespecNotFoundError,检查模块名和函数名、参数数量(arity)是否写对
  2. 匿名函数类型只校验 arity:typespec 里声明的函数类型参数,只检查参数个数,不检查参数和返回的具体类型

小结

use Hammox.Protect用一行代码换来了完整的运行时契约保障:自动收集 behaviour 回调、自动生成受保护的本地函数、类型不匹配立即报错。对新手而言,它把"测试实现是否遵守 behaviour 契约"这件容易遗漏的事,变成了零心智负担的默认行为。

想进一步了解 Hammox 的 Telemetry 事件与可观测性,可以阅读项目自带的指南guides/Telemetry.md;核心宏的完整源码在lib/hammox/protect.ex,建议对照本文阅读一遍,你会对"宏生成函数"这套 Elixir 魔法有更深的体会。

【免费下载链接】hammox🏝 automated contract testing via type checking for Elixir functions and mocks项目地址: https://gitcode.com/gh_mirrors/ha/hammox

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

http://www.cnnetsun.cn/news/4158486.html

相关文章:

  • heroku_san安装教程:Rails 3+、Rails 2与Sinatra三种框架如何快速接入Heroku部署
  • PyMacroRecord:免费跨平台宏录制工具,重复操作一键回放
  • 深入 Duster 架构:基于 Laravel Zero 与适配器模式统一 4 大 Lint 工具的设计之道
  • CoopCycle餐厅配置实战:5步搞定菜单、商品与在线点餐的完整教程
  • HDR环境光照教程:用intensity_env和rotate_env在rayshader_portraits中打造电影级光影
  • 基于FMMosaicLayout打造互动图片墙:Cell长按缩放动画与复用机制实战
  • tlock进阶技巧:Duration、轮次编号与Armor,6个让时间锁加密更灵活的用法
  • 题解:洛谷 P3076 [USACO13FEB] Taxi G
  • NCM 转 MP3 完整攻略:用 ncmdump 免费转换,拖拽 4 步上手
  • FiD显存优化秘籍:Checkpointing与answer_maxlength如何驯服100段长文本
  • 毕业论文“难产”自救指南:AI写论文哪个软件最好?我站宏智树AI
  • UE5-MCP:如何用AI把3个月的UE5关卡开发压缩到3天
  • 星际争霸II Bot API库python-sc2入门:为什么它是Python打造SC2 AI机器人的终极选择
  • 为什么Remote PowerShell正在被淘汰:理解Exchange V3模块的REST API连接迁移(office-docs-powershell)
  • 053、VLA模型的训练数据与配比:互联网数据与机器人数据的融合
  • 3步备份QQ空间全部历史说说:GetQzonehistory 完整上手指南
  • QuickLook 插件选型指南:macOS 空格键预览只装这几组才真正用得上
  • PCSX2 Gamefixes与PNACH作弊码完全指南:解决闪退卡顿并解锁60帧
  • Engauge Digitizer 入门指南:从图表图像提取数据点的安装配置全流程
  • CSWin Transformer预训练权重怎么选:Tiny/Small/Base/Large六种模型参数与FLOPs全对比(附选型建议)
  • 如何快速给 Unity WebGL 加上原生输入框:WebGLInput 完整上手指南
  • Oracle APEX Blueprints实战:AI驱动的规范开发,从需求文档一键生成应用
  • EntityFrameworkCore.Triggered性能开销到底有多大?完整基准测试数据解读
  • 132、洞察驱动的实战标题——时域降噪的“鬼影博弈“——运动检测阈值高一点还是低一点?从运动矢量置信度到混合权重的工程调优
  • 使用vminpoly前必知的5个注意事项:常见坑与浏览器兼容清单
  • 扩展 D-Zone:接入 Slack 等新聊天平台的开发者进阶指南
  • 潍坊全家电维修服务指南-欧米到家常见故障、服务范围与预约报修
  • SillyTavern-Launcher:一条命令装好 AI 应用全家桶|从零跑通完整指南
  • 安卓7.0 开机动画和launcher之间的黑屏...如何解决?
  • R2CNN_Faster-RCNN_Tensorflow网络架构深度剖析:ResNet+RPN双路旋转检测的TensorFlow实现原理