Ruby Spec Suite(ruby/spec)完全指南:用可执行的代码描述 Ruby 行为规范
【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby
导读
本文以仓库内 spec/ruby/README.md 为主干,系统讲解 Ruby Spec Suite(简称 ruby/spec)这一用可执行代码描述 Ruby 语言行为的测试套件:它的定位与动机、目录组织方式、MSpec 运行器的使用、在不同 Ruby 实现上运行与同步的机制,以及编写规范(matchers、guards、shared specs)的完整实践。读完本文,你将掌握如何在本仓库(CRuby/MRI 源码树)中运行、筛选与编写 Ruby 行为规范,并理解语言、核心库、标准库与 C 扩展 API 四大规范体系是如何在 spec/ruby/default.mspec 中组织起来的。
Ruby Spec Suite 是什么
Ruby Spec Suite,缩写为ruby/spec,是一套用于描述Ruby 编程语言行为的测试套件。它不是类似 ISO 那样的标准化规范,也不以成为正式标准为目标——而是一个用代码描述、测试 Ruby 行为的实用工具。
每条示例代码都带文本描述
套件中的每一个示例(example)都附带一段文字描述,这带来三方面优势:
- 更容易理解作者的意图:阅读者不必靠猜,就能知道这段断言想验证什么;
- 文档化最新版 Ruby 的行为:规范本身即是可执行的文档,描述“当前 Ruby 应当如何表现”;
- 帮助各 Ruby 实现达成行为一致:MRI、JRuby、TruffleRuby 等实现都以其为共同行为基线。
规范采用与RSpec 2相似的语法书写,并通过MSpec——为运行 Ruby Spec Suite 而专门构建的测试框架——来执行。本仓库在 spec/mspec 目录下自带了一份 MSpec 的 vendored 副本(包含lib/、spec/、tool/等子目录),便于直接在 CRuby 源码树内运行规范。
覆盖范围与分组方式
规范覆盖了以下五个领域,分别对应仓库中的目录:
| 领域 | 目录 | 内容示例 |
|---|---|---|
| 语言语法 | spec/ruby/language | if、def、A::B、for、while、rescue、正则/字符串字面量等 |
| 核心库 | spec/ruby/core | Integer#+、String#upcase等,无需 require 即可使用的方法 |
| 标准库 | spec/ruby/library | CSV.new、YAML.parse等,需要 require stdlib 的方法 |
| C 扩展 API | spec/ruby/optional/capi | C 扩展可调用的 Ruby C API 函数 |
| 命令行参数 | spec/ruby/command_line | ruby 可执行文件的-v、-e等命令行标志 |
语言规范按关键字分组(例如if_spec.rb、def_spec.rb、class_spec.rb),核心库与标准库规范按类和方法分组(例如core/kernel/、library/csv/)。关于语言侧的分类哲学,可参见 spec/ruby/language/README:它主张“与其用计算理论的概念组织规范,不如直接用 Ruby 语言的实体(字面量、保留字、变量)来组织”,并指出false/true/nil/self归入predefined_spec.rb、in归入for_spec.rb、then/elsif归入if_spec.rb、when归入case_spec.rb、catch归入throw_spec.rb等合并规则。
与各 Ruby 实现的 CI 关系
README 声明,ruby/spec 在每次提交时都会经过以下实现的测试:
- MRI:在 30 个平台、4 个版本上测试;
- JRuby(1.7 与 9.x);
- TruffleRuby;
- Opal;
- Artichoke。
ruby/spec 描述的是Ruby 3.3 及更新版本的行为。更精确地说,每一个最新稳定版 MRI 发行版(3.3.x、3.4.x 等)都应在 CI 中通过 ruby/spec 的全部规范。
与 Ruby 实现的月度双向同步
ruby/spec 与 MRI、JRuby、TruffleRuby 之间每月进行一次双向同步,各仓库都在自己的spec/ruby目录下保留一份规范的完整副本,以方便就地编辑。这意味着:
- 想为某个实现测试开发版时,应当使用该实现自身
spec/ruby下的副本——那才是其 CI 真正测试的版本; - 本仓库(ruby/spec 的上游来源之一)不一定包含 MRI 最新的规范变更(同步是月度的),也不包含 tags(标记为在该实现上失败的规范)。
在某个 Ruby 实现上运行规范的通用方式是:
$ cd ruby_implementation/spec/ruby # 将 ../ruby_implementation/bin 加入 PATH,或用 -t /path/to/bin/ruby 指定 $ ../mspec/bin/mspec在 CRuby 源码树中,这套规范位于 spec/ruby,与源码根目录的default.mspec(见下文“在 CRuby 构建树中运行”)配合使用。
运行规范:从零开始
第一步:获取 ruby/spec 与 MSpec
README 给出的最小启动步骤是:
$ git clone https://github.com/ruby/spec.git $ cd spec $ git clone https://github.com/ruby/mspec.git ../mspec $ ../mspec/bin/mspec最后一条命令会用当前PATH中名为ruby的可执行文件运行全部规范。
指定具体 Ruby 实现
用-t选项指定运行规范所用的 Ruby 实现,参数可以是 Ruby 二进制文件的完整路径,也可以是$PATH中的可执行名:
$ ../mspec/bin/mspec -t /path/to/some/bin/ruby这在使用miniruby、ruby-debug或自定义构建版本做回归验证时尤其有用。
运行选定的规范
mspec接受文件、目录与分组三种粒度:
# 单个规范文件 $ ../mspec/bin/mspec core/kernel/kind_of_spec.rb # 整个目录 $ ../mspec/bin/mspec core/kernel # 按 default.mspec 中定义的分组运行 $ ../mspec/bin/mspec :language $ ../mspec/bin/mspec :core $ ../mspec/bin/mspec :library $ ../mspec/bin/mspec :capi分组在 spec/ruby/default.mspec 中定义,其中不仅包含 README 提到的四组,还扩展了更多:
| 分组键 | 目录 | 说明 |
|---|---|---|
:language | language | 语言特性规范 |
:core | core | 核心库规范 |
:library | library | 标准库规范 |
:command_line | command_line | 命令行规范 |
:security | security | 安全相关规范 |
:capi | optional/capi | C 扩展 API 规范 |
:thread_safety | optional/thread_safety | 线程安全规范 |
:optional | capi + thread_safety | 全部可选规范 |
:files | 以上全部 | 实际运行的目录顺序(command_line → language → core → library → security → optional) |
:ci_files | 同:files | mspec ci运行时使用的文件集合 |
此外,default.mspec还设定了set :target, 'ruby'(默认实现)、tags_patterns(将language/、core/、library/等路径映射到tags/下的 tag 文件,将_spec.rb映射到_tags.txt),以及toplevel_constants_excludes(运行泄漏检查时豁免\wSpecs?$、^CS_CONST、^CSL_CONST、^Prism等顶层常量)。
泄漏检查(Sanity Checks)
运行规范时可以对多种“泄漏”开启检查:文件描述符、临时文件、线程、子进程、ENV、ARGV、全局编码、顶层常量。启用方式:
$ CHECK_LEAKS=true ../mspec/bin/mspec关于顶层常量的规范:
- 新的顶层常量只在必要时引入,或遵循
<ClassBeingTested>Specs模式,例如module StringSpecs; - 其他用于测试的常量应嵌套在这样的模块之下;
- 例外情况记录在 spec/ruby/.mspec.constants 文件中;
- 可以用
CHECK_LEAKS=save让 MSpec自动把新增的顶层常量追加进该文件:
$ CHECK_LEAKS=save mspec ../mspec/bin/mspec files390x 架构上的 zlib 相关问题
在 s390x CPU 架构上,如果看到与 zlib 库相关的失败规范,可以加上DFLTCC=0运行。这类失败可能源于 zlib 应用了 madler/zlib#410 补丁后,deflate 算法产出了不同的压缩字节流:
$ DFLTCC=0 ../mspec/bin/mspec运行所需的外部依赖
规范运行依赖以下命令行可执行文件:
echostat(用于core/file/*time_spec.rb)find(用于core/file/fixtures/file_types.rb,来自findutils包,Windows 上不需要)
socket 相关规范还需要文件/etc/services(Debian 上来自netbase包,Windows 上不需要)。
在 CRuby 构建树中运行规范
本仓库作为 CRuby/MRI 源码树,其构建系统集成了 ruby/spec 的运行配置。根目录的 default.mspec 与 spec/default.mspec 展示了与上游 ruby/spec 略有差异的 MRI 化配置:
- 将默认
:target设置为构建目录下的miniruby; - 通过
runruby.rb与--archdir/--extout传递构建目录与扩展输出路径,确保规范针对当前构建而不是系统安装的 ruby 运行; - 动态构造
:library分组:把 gems/bundled_gems 中列出的捆绑 gem 对应的规范(openstruct会映射为ostruct)从标准库规范中剔除,分别归入:bundled_gems与:stdlibs; - 默认开启常量泄漏检查(
ENV["CHECK_CONSTANT_LEAKS"] ||= "true"),并注入-W:no-experimental以确保子进程输出按原样断言; - 内置
MSpecScript::JobServer,可借用测试框架的 jobserver 并行调度cores; - 通过 prepend 定制
DottedFormatter,在终端上按固定列宽打印文件进度与点数。
也就是说,在本仓库构建完成后,你可以直接在源码根目录运行make test-spec之类的目标(参见 common.mk 与 defs/gmake.mk),或手动以 default.mspec 作为配置启动 MSpec。规范自身的引导逻辑见 spec/ruby/spec_helper.rb:它会校验VersionGuard::FULL_RUBY_VERSION >= SpecVersion.new('3.3')(低于 3.3 会直接abort),并支持在未设置MSPEC_RUNNER时直接用ruby some_spec.rb的方式运行单个规范(此时会加载mspec/commands/mspec-run并执行MSpecRun.main)。
如何编写规范:从 CONTRIBUTING.md 看最佳实践
编写与贡献规范的完整文档见 spec/ruby/CONTRIBUTING.md。以下要点均出自该文件,可直接套用。
文件组织:按方法的 owner 决定归属
规范分为 5 个顶层分组(command_line、language、core、library、optional/capi),而某个方法归属哪个文件,由其#owner决定。例如:
> [].method(:group_by) => #<Method: Array(Enumerable)#group_by> > [].method(:group_by).owner => Enumerable因此group_by应写在core/enumerable/group_by_spec.rb,而不是core/array/下。
用 mkspec 生成规范骨架
MSpec 附带的mkspec工具可用来生成规范结构:
$ ../mspec/bin/mkspec -h为尚未规范的模块或类创建文件,例如为forwardable生成规范:
$ ../mspec/bin/mkspec -b library -rforwardable -c Forwardable-b指定core或library作为基准分组。
查找尚未覆盖的核心方法也很简单,在spec目录下执行(ruby需为较新的 MRI):
$ ruby --disable-gem ../mspec/bin/mkspec也可以搜索it "needs to be reviewed for spec completeness"——该文案表示文件已生成但方法尚未被规范覆盖。
Matchers:should语法
规范的基本理念是:在期望为真的谓词前加上.should即可。这套语法直接调用 Ruby 原有的比较方法,失败时能给出清晰的错误,也无需像 RSpec 那样记忆eq/==的映射关系。
比较类 matcher:
(1 + 2).should == 3 # 调用 #== (1 + 2).should_not == 5 File.should.equal?(File) # 调用 #equal?(测试同一性) (1 + 2).should.eql?(3) # 调用 #eql?(Hash 相等性) 1.should < 2 2.should <= 2 3.should >= 3 4.should > 3 "Hello".should =~ /l{2}/ # 调用 #=~(正则匹配)谓词类 matcher:
[].should.empty? [1,2,3].should.include?(2) "hello".should.start_with?("h") "hello".should.end_with?("o") (0.1 + 0.2).should be_close(0.3, TOLERANCE) # (0.2-0.1).abs < TOLERANCE (0.0/0.0).should.nan? 3.14.should.instance_of?(Float) # 调用 #instance_of? 3.14.should.is_a?(Numeric) # 调用 #is_a? 3.14.should.respond_to?(:to_i) Integer.should.method_defined?(:+, false)异常类 matcher:
-> { raise "oops" }.should.raise(RuntimeError, /oops/) -> { raise "oops" }.should.raise(RuntimeError) { |e| # 对 Exception 对象做自定义检查 e.message.should.include?("oops") e.cause.should == nil }需要注意should_not.raise应当尽量避免:与其断言“不抛异常”,不如断言 lambda 中代码的实际结果;一旦真的抛异常,示例本来就会失败。
警告 matcher:
-> { Fixnum }.should complain(/constant ::Fixnum is deprecated/) # 期望产生警告一个真实的例子是 core/kernel/kind_of_spec.rb,它用should ==验证kind_of?是is_a?的别名:
require_relative '../../spec_helper' describe "Kernel#kind_of?" do it "is an alias of Kernel#is_a?" do Kernel.instance_method(:kind_of?).should == Kernel.instance_method(:is_a?) end endGuards:按版本、平台与 bug 情况裁剪规范
规范中使用各种 guard 来限定适用范围,最常见的有:
版本 guard:
ruby_version_is ""..."3.2" do # RUBY_VERSION < 3.2 的规范 end ruby_version_is "3.2" do # RUBY_VERSION >= 3.2 的规范 end平台 guard:
platform_is :windows do # 仅 Windows 有效 end platform_is_not :windows do # Windows 之外都有效 end platform_is :linux, :darwin do # OR 语义 end platform_is_not :linux, :darwin do # 既不是 Linux 也不是 Darwin end platform_is pointer_size: 64 do # 64 位平台 end big_endian do # 大端平台 endbug guard(ruby_bug):仅当 MRI 存在 bug、且修复会被 backport 到旧版本时使用。使用前先在 https://bugs.ruby-lang.org/ 提交 bug。其语义等价于guard_not { RUBY_ENGINE == "ruby" && ruby_version_is ''...'X.Y' },即在存在 bug 的指定 MRI 版本上跳过,而在替代实现上执行:
ruby_bug '#13669', ''...'3.2' do it "works like this" do # 这里应描述预期行为,而不是 bug 本身 end end组合 guard 与自定义 guard:
guard -> { platform_is :windows and ruby_version_is ""..."3.2" } do # Windows 且 RUBY_VERSION < 3.2 end guard_not -> { platform_is :windows and ruby_version_is ""..."3.2" } do # 相反情况 end max_uint = (1 << 32) - 1 guard -> { max_uint <= fixnum_max } do end自定义 guard 优于普通if,因为 guard 能让mspec的命令(如 tag 相关命令)正常工作。CONTRIBUTING.md 特别强调:没有用于定义“实现特有行为”的 guard——Ruby Spec Suite 定义的是共同行为而非实现细节,实现特有行为应放到各实现自己的测试套件;若某实现不支持某特性,把相关规范打上 failing tag 即可。
Shared Specs:消除重复规范
当多个方法/模块具有相同行为时,用 shared specs 复用规范,避免重复。
- 仅在本模块内复用的 shared spec,放在该模块目录下的
shared/子目录中,例如core/hash/shared/iteration.rb; - 跨模块/类复用的,放在顶层 spec/ruby/shared,例如
shared/file/socket.rb被core/file/socket_spec.rb、core/filetest/socket_spec.rb等共同使用。
定义时在顶层describe上加shared: true选项,表示该块不被 runner 直接执行;shared spec 通过实例变量@method和@object接收调用方传入的参数:
# core/hash/shared/iteration.rb describe :hash_iteration_no_block, shared: true do it "returns an Enumerator if called on a non-empty hash without a block" do { 1 => 2 }.send(@method).should.instance_of?(Enumerator) end end # core/hash/select_spec.rb describe "Hash#select" do it_behaves_like :hash_iteration_no_block, :select end # core/hash/reject_spec.rb describe "Hash#reject" do it_behaves_like :hash_iteration_no_block, :reject end当 shared spec 需要比“一个对象”更多的上下文时,可以传入 lambda,它会拥有实现方 spec 的作用域:
describe :kernel_sprintf, shared: true do it "raises TypeError exception if cannot convert to Integer" do -> { @method.call("%b", Object.new) }.should.raise(TypeError) end end describe "Kernel#sprintf" do it_behaves_like :kernel_sprintf, -> format, *args { sprintf(format, *args) } end风格上,CONTRIBUTING.md 要求不遗留行尾空格,并遵循现有风格。
历史沿革:从 Rubinius 到 RubySpec 再到 Ruby Spec Suite
- 项目最初源自Rubinius的测试被改写为 spec 风格;
- 这些规范后来被独立出来成为RubySpec项目,拥有自己的愿景与原则;
- 2014 年底,RubySpec 的创建者 Brian Shirai 宣布终止 RubySpec;
- 几个月后,多个相关仓库被合并,项目得以复活;
- 2016 年 1 月 12 日,项目更名为 “The Ruby Spec Suite”,让 RubySpec 的旧意识形态成为历史。
另外,spec/ruby/library 下的大多数 socket 规范源自rubysl-socket项目(已不在 GitHub 上)。该项目的三位版权持有者 Yorick Peterse、Chuck Remes 与 Brian Shirai 已同意将这些规范在 MIT 许可下重新授权给 ruby/spec,因此可以在本仓库中继续使用与修改。
结语
Ruby Spec Suite 既是测试套件,也是一份始终与代码同步的可执行语言文档。在 CRuby 源码树中,spec/ruby 目录、default.mspec 分组配置与 CONTRIBUTING.md 编写规范构成了一个完整的闭环:用 MSpec 运行、用 tag 管理实现差异、用 shared specs 消除重复、用 matchers 与 guards 精确表达预期。无论你是要验证某个 Ruby 实现的行为一致性,还是想为语言新特性补充规范,这套方法论都值得直接复用。
【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考