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

cyri_ · November 05, 2026 · 9 hits

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 是它第一次让我觉得没有什么可以再去掉的。欢迎试用,欢迎反馈。

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.