大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
测试与质量
这一课你将能做到
- 用 Minitest 写单元测试,用 RSpec 写行为驱动测试(BDD)
- 用 Pry 和 Byebug 调试 Ruby 代码,设置断点和检查变量
- 用 RuboCop 统一代码风格,用 YARD 写文档注释
为什么要学
Ruby 社区有「测试文化」——不写测试的 Ruby 代码等于没写完。Rails 的 rails test、CI 流水线里的 rspec、代码审查里的 RuboCop 检查,测试和质量工具是 Ruby 开发者的日常。不写测试,代码不敢重构;不用 RuboCop,团队风格不统一。
人话:测试是「安全网」——改代码时不怕摔死;RuboCop 是「交警」——统一交通规则;Pry 是「透视镜」——运行时看代码内部。三者配合,代码从「能跑」变成「可维护」。
先建立图像
Ruby 质量工具链
┌─────────────┬─────────────────────────────────┐
│ 测试框架 │ Minitest(标准库) / RSpec(BDD) │
│ 断言库 │ Minitest::Assertions / RSpec::Matchers │
│ Mock/Stub │ Minitest::Mock / RSpec Mocks │
│ 调试器 │ Pry(交互式) / Byebug(断点) │
│ 代码风格 │ RuboCop / Standard │
│ 文档 │ YARD / RDoc │
│ 覆盖率 │ SimpleCov │
└─────────────┴─────────────────────────────────┘
跟着做
步骤 1:Minitest——标准库测试
# calculator.rb
class Calculator
def add(a, b)
a + b
end
def divide(a, b)
raise ArgumentError, "除数不能为 0" if b.zero?
a / b
end
end
# test_calculator.rb
require "minitest/autorun"
require_relative "calculator"
class TestCalculator < Minitest::Test
def setup
@calc = Calculator.new
end
def test_add
assert_equal 5, @calc.add(2, 3)
end
def test_divide
assert_equal 2, @calc.divide(10, 5)
end
def test_divide_by_zero
assert_raises(ArgumentError) { @calc.divide(10, 0) }
end
def test_add_floats
assert_in_delta 0.3, @calc.add(0.1, 0.2), 0.0001
end
end
# 运行:ruby test_calculator.rb
Minitest 断言速查:
assert_equal expected, actual # 相等
assert_nil obj # 为 nil
assert_includes collection, obj # 包含
assert_raises(Exception) { ... } # 抛异常
assert_in_delta expected, actual, delta # 浮点数近似
refute condition # 断言为假
步骤 2:RSpec——行为驱动测试
# Gemfile
# gem "rspec", "~> 3.12"
# spec/calculator_spec.rb
require_relative "../calculator"
RSpec.describe Calculator do
let(:calc) { Calculator.new } # 懒加载,每个示例独立
describe "#add" do
it "返回两数之和" do
expect(calc.add(2, 3)).to eq(5)
end
it "支持浮点数" do
expect(calc.add(0.1, 0.2)).to be_within(0.0001).of(0.3)
end
end
describe "#divide" do
it "返回商" do
expect(calc.divide(10, 5)).to eq(2)
end
it "除数为 0 时抛出 ArgumentError" do
expect { calc.divide(10, 0) }.to raise_error(ArgumentError, "除数不能为 0")
end
end
end
# 运行:rspec spec/calculator_spec.rb
# 格式化输出:rspec --format doc
RSpec 匹配器速查:
expect(actual).to eq(expected) # 值相等
expect(actual).to be > 5 # 比较
expect(actual).to be_truthy # 真值
expect(actual).to be_nil # nil
expect(collection).to include(item) # 包含
expect { ... }.to raise_error(Error) # 异常
expect { ... }.to change { obj.attr }.by(1) # 状态变化
步骤 3:Mock 与 Stub
# RSpec Mocks
RSpec.describe OrderService do
it "调用支付网关" do
gateway = instance_double(PaymentGateway)
allow(gateway).to receive(:charge).and_return(success: true)
service = OrderService.new(gateway)
result = service.process(100)
expect(result).to be true
expect(gateway).to have_received(:charge).with(100)
end
end
# Minitest Mock
require "minitest/mock"
def test_with_mock
mock = Minitest::Mock.new
mock.expect(:charge, { success: true }, [100])
service = OrderService.new(mock)
service.process(100)
mock.verify # 验证 mock 被正确调用
end
步骤 4:Pry 与 Byebug 调试
# gem install pry pry-byebug
# 在代码中打断点
require "pry"
def complex_calculation(data)
result = data.map { |x| x * 2 }
binding.pry # 程序暂停,进入交互式控制台
result.sum
end
# 进入 Pry 后常用命令:
# ls # 列出当前作用域变量
# cd obj # 进入对象内部
# show-doc # 查看方法文档
# edit # 编辑当前文件
# continue # 继续执行(Byebug)
# step/next # 单步执行(Byebug)
# Byebug 命令行调试
# ruby -r byebug script.rb
# 或在代码中写 byebug
步骤 5:RuboCop 与代码风格
# 安装
gem install rubocop
# 检查当前目录
rubocop
# 自动修复(安全)
rubocop -a
# 自动修复(包括有风险)
rubocop -A
# 生成配置文件
rubocop --auto-gen-config
.rubocop.yml 示例:
AllCops:
TargetRubyVersion: 3.3
NewCops: enable
Style/FrozenStringLiteralComment:
Enabled: true
Metrics/MethodLength:
Max: 20
Metrics/ClassLength:
Max: 150
常见 RuboCop 规则:
| 规则 | 检查内容 |
|---|---|
| Style/StringLiterals | 单引号 vs 双引号 |
| Metrics/MethodLength | 方法长度 |
| Metrics/AbcSize | 方法复杂度 |
| Lint/UnusedVariable | 未使用变量 |
| Style/Documentation | 类/模块缺少注释 |
讲清楚要点
- Minitest 是标准库,RSpec 是 DSL:Minitest 随 Ruby 自带,语法直接;RSpec 用 describe/context/it 组织测试,读起来像自然语言。Rails 默认用 Minitest,社区项目多用 RSpec。
- let vs before:RSpec 的
let是懒加载(用到才创建),before是每个示例前执行。let更灵活,但过度嵌套会让测试难以追踪。 - Mock 是「假对象」,Stub 是「假方法」:Mock 验证「方法被调用」(行为验证);Stub 只控制「方法返回值」(状态验证)。RSpec 的
instance_double会验证方法签名,比double更安全。 - RuboCop 的
-a和-A:-a只修复「安全」的规则(如空格、引号);-A包括「有风险」的规则(如方法长度拆分)。CI 里用-a,本地大胆用-A。 - SimpleCov 覆盖率不是目标:100% 覆盖率不等于无 bug,只代表代码被执行过。追求「有意义的测试」而非「覆盖率的数字」。
你可能会懵的地方
-
Minitest 和 RSpec 哪个好? 没有绝对。Minitest 更轻量、标准库自带;RSpec 表达力强、生态丰富。Rails 项目默认 Minitest,但换成 RSpec 也很常见。团队统一即可。
-
let和let!的区别?let懒加载,第一次引用时执行;let!立即执行,在每个示例前强制创建。有副作用(如写数据库)的用let!。 -
RSpec 的
subject是什么?subject是被测试对象的默认实例。subject { described_class.new }或自动推断。is_expected.to eq(5)等价expect(subject).to eq(5)。 -
Pry 和 irb 的区别? Pry 是增强版 irb:语法高亮、命令系统(
show-doc、edit)、插件生态(pry-byebug、pry-rails)。调试时 Pry 比 irb 强大得多。 -
RuboCop 和 Standard 的区别? RuboCop 可配置,规则细;Standard 是 RuboCop 的「零配置」封装,意见统一,不让你纠结配置。新项目推荐 Standard,老项目用 RuboCop 渐进改造。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
测试里用 puts 调试 | 输出污染、无法断言 | 用 binding.pry 或 expect 断言 |
assert_equal expected, actual 参数写反 | 失败信息 confusing | 第一个参数是 expected |
RSpec 里 let 嵌套 5 层 | 测试难以追踪数据来源 | 减少嵌套,用 before 显式创建 |
用 double 不验证方法签名 | 方法改名后测试还过 | 用 instance_double 或 class_double |
RuboCop 全部 -A 自动修复 | 可能改坏代码逻辑 | 先 -a,再人工 review -A 的变更 |
卡点急救
- 测试随机失败:顺序依赖或共享状态。用
rspec --order random发现,检查let和before的副作用。 - Pry 进不去:
require "pry"没写,或代码路径没走到。加puts "here"确认执行到断点。 - RuboCop 报大量历史遗留问题:用
rubocop --auto-gen-config生成.rubocop_todo.yml,逐步修复。
面试常问
-
TDD 和 BDD 的区别? TDD(测试驱动开发)先写测试再写实现,关注「代码是否正确」;BDD(行为驱动开发)用自然语言描述行为,关注「系统行为是否符合预期」。RSpec 是 BDD 框架,
describe/it语法让测试读起来像需求文档。 -
Mock 和 Stub 的区别? Stub 控制「方法的返回值」(状态验证);Mock 验证「方法是否被调用、参数是否正确」(行为验证)。Stub 是「 passive 的」,Mock 是「active 的」。
-
如何写出可维护的测试? 测试代码也要 DRY,但不要过度抽象;每个测试只验证一件事;测试名描述行为而非实现;用
let管理测试数据,但避免深层嵌套;集成测试比单元测试更能发现问题。
自测三问
let和before在 RSpec 中的执行时机有什么区别?instance_double比double更安全,为什么?- RuboCop 的
-a和-A分别修复什么类型的规则?
小结一句话
测试是安全网,Pry 是透视镜,RuboCop 是交警——三者配合,Ruby 代码从「能跑」进化到「可维护、可重构、可协作」。
延伸
- RSpec 官方文档
- RuboCop 官方文档
- 下一课:综合练习——用 Bundler 创建 Gem,写一个完整的 CLI 小项目。