Ruby Fix 1.0:先写规范,再测试

cyri_ · 2026年11月05日 · 8 次阅读

Fix 1.0 发布了。

Fix 是一个 Ruby 的规范框架。它让你用 RFC 2119 的词汇——MUST、SHOULD、MAY——写下一个对象应该做什么,而不涉及它是怎么实现的。规范写好之后,拿任何一个对象去测试它,每条期望得到一个结果。

一个例子

require "fix"

class Duck
  def walk = "Klop klop!"
  def swim = "Swoosh..."
end

Fix :Duck do
  on :walk do
    it MUST eq "Klop klop!"
  end

  on :swim do
    it MUST eq "Swoosh..."
  end

  on :speak do
    it MAY eq "Quaaaaaack!"
  end
end

exit(Fix[:Duck].test { Duck.new })
$ ruby duck_fix.rb
duck_fix.rb:10: Success: expected "Klop klop!" to eq "Klop klop!"
duck_fix.rb:14: Success: expected "Swoosh..." to eq "Swoosh..."
duck_fix.rb:18: Info: NoMethodError: undefined method 'speak' for an instance of Duck
$ echo $?
0

Fix :Duck do … end 构建一份规范,并以这个名字注册;Fix[:Duck] 把它找回来。on :walk 向主体发送 walk,里面的 it 判断返回值。it 后面不是块,而是一个要求:一个级别,然后一个匹配器。

鸭子不会说话,但规范只说它 MAY 说话:方法不存在,结果是一条 Info,规范依然满足,进程退出码为 0。换成 MUST,就是一条 Error,退出码为 1。

五种结果

两个问题把级别区分开来:方法存在吗?MUST 和 SHOULD 要求它存在,MAY 不要求。它做了规范说的事吗?MUST 和 MAY 要求做到,SHOULD 只是建议。于是有五种结果:

级别 匹配 不匹配 方法不存在 其他异常
MUST Success Failure Error Error
SHOULD Success Warning Error Error
MAY Success Failure Info Error

Success、Warning、Info 通过;Failure、Error 不通过。

设计上的几个选择

  • 规范与主体分开。 规范是一份文档,加载时读一遍;主体由 test 的块构建。同一份规范可以测试 Duck.new,也可以测试任何别的对象。
  • 每个上下文一个子进程。 每个有期望的上下文都在 fork 出来的子进程里运行:主体留下的任何状态都碰不到下一个上下文,主体抛出的异常(包括 exit)只是它那一条期望的错误。
  • 输出只有结果。 每条期望一行:位置: 结果: 摘要,错误的 backtrace 缩进在下面。没有颜色,没有点,没有计数。test 返回是否全部通过,exit 把它变成进程的退出码。
  • 很小。 lib/ 下 9 个文件,约 300 行代码。Fix 站在四个各自独立的 gem 上:Aw(在子进程里运行一个块,带回结果)、Defi(把一次方法调用当作值)、Matchi(匹配器)、Spectus(RFC 2119 的级别与判定)。

规范还可以用 let 和 with 给主体传参数,可以用 fix(:Name) 要求另一份规范。fix-command 提供 fix 命令,把 fix/ 目录下的每个 *_fix.rb 各自放在一个进程里运行。这些都在 README 里。

开始

gem "fix"

需要 Ruby 3.3 或更高版本,以及一个支持 fork 的系统。

这个项目我写了十二年,1.0 是它第一次让我觉得没有什么可以再去掉的。欢迎试用,欢迎反馈。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号