RSpec 深入解析
ruby group :development, :test do gem 'rspec', '~> 3.12' end
执行安装并生成默认配置文件:
```bash
bundle install
rspec --init
这会在项目根目录生成 .rspec(运行选项)和 spec/spec_helper.rb(全局配置)。
通常还会配合 Rails 使用,会额外生成 rails_helper.rb。之后所有测试文件放在 spec/ 目录下,文件名以 _spec.rb 结尾。
测试的基本骨架:describe、context、it
RSpec 通过三个核心词构建测试层级,形成一棵可读的树状结构:
describe Calculator do # 被测试的类或行为
describe '#add' do # 实例方法
context 'with positive numbers' do # 特定条件
it 'returns the sum' do # 具体断言
calc = Calculator.new
expect(calc.add(2, 3)).to eq(5)
end
end
context 'with negative numbers' do
it 'returns the correct negative sum' do
calc = Calculator.new
expect(calc.add(-2, -3)).to eq(-5)
end
end
end
end
describe:描述被测对象(类、方法、行为),可嵌套。context:描述状态或条件,通常以 “when”, “with”, “without” 开头,是describe的别名,纯粹为了语义化。it:描述期望的行为,块内编写具体的测试代码。
运行 rspec spec/calculator_spec.rb 会输出清晰的文本描述。
期望写法与常用匹配器
RSpec 3 之后推荐使用 expect 语法。基本形式为:
expect(实际值).to 匹配器
expect(实际值).not_to 匹配器
等价性与比较匹配器
expect(1 + 1).to eq(2) # 值相等
expect(1 + 1).to eql(2) # 值相等,类型也相同(Ruby中通常和eq相同)
expect("abc").to equal("abc") # 严格对象相同(equal?)
expect(5.0).to be_within(0.1).of(5.0) # 浮点比较
expect(10).to be > 5
expect(10).to be <= 10
expect(10).to be_between(5, 15).inclusive # 范围包含
真实性、空值与类型匹配器
expect("hello").to be_truthy # 不是 nil 或 false
expect(nil).to be_falsy # 是 nil 或 false
expect(nil).to be_nil
expect([]).to be_empty
expect("abc").to start_with("a")
expect("abc").to end_with("c")
expect([1,2,3]).to include(2)
expect({a: 1}).to have_key(:a)
expect(100).to be_an_instance_of(Integer)
expect(100).to be_a(Numeric) # 包含继承
错误与变化匹配器(副作用测试)
expect { 1 / 0 }.to raise_error(ZeroDivisionError)
expect { counter.increment }.to change { counter.value }.from(0).to(1)
expect { counter.increment }.to change { counter.value }.by(1)
expect { ... } 块形式用于捕获异常或监测对象变化。
谓词匹配器(自动映射)
任何以 be_ 开头的匹配器会调用谓词方法(has_ 前缀同理)。
expect(5).to be_odd # 调用 5.odd?
expect(user).to be_active # 调用 user.active?
expect(user).to have_role(:admin) # 调用 user.has_role?(:admin)
测试准备与数据隔离:before、after 与 let
before / after 钩子
在同一个 describe 或 context 块内,钩子会在每个示例(it)的前后执行。
describe Order do
before(:each) do
@order = Order.new # 每个示例都有全新的 @order
end
after(:each) do
DatabaseCleaner.clean # 清除数据
end
it 'can add items' do
@order.add_item("book")
expect(@order.items.count).to eq(1)
end
end
常用钩子::each(默认)、:all(整个上下文只运行一次,慎用,会导致示例间依赖)、:suite(全局)。
let 与 let!
let 是惰性求值的辅助方法,可替代实例变量,让测试更干净。
describe User do
let(:user) { User.new(name: "Bob") }
it 'has a name' do
expect(user.name).to eq("Bob") # 首次调用 user 时执行 user = User.new(...)
end
end
let只在第一次被引用时计算,并在每个示例中缓存结果。let!会立即求值(类似before),适用于需要确保记录已存在于数据库的场景(如关联测试)。- 多个
let可以互相引用,形成直观的依赖关系。
subject
subject 可以看作是默认的 let 定义,用于指明测试对象。RSpec 支持隐式自动生成,但建议显式声明。
describe Person do
subject { Person.new("Alice", 30) }
it 'has a name' do
expect(subject.name).to eq("Alice") # 显式使用 subject
end
# 可与 is_expected 配合,更简洁
it { is_expected.to have_attributes(name: "Alice") }
end
消除重复代码:共享示例
当多个类或方法遵循相同契约时,shared_examples 可避免复制粘贴。
shared_examples 'a sortable collection' do
it 'responds to sort' do
expect(collection).to respond_to(:sort)
end
end
describe Array do
let(:collection) { [3,1,2] }
it_behaves_like 'a sortable collection'
end
describe Set do
let(:collection) { Set.new([3,1,2]) }
it_behaves_like 'a sortable collection'
end
也可以传递参数给共享示例:
shared_examples 'an enumerable' do |count|
it "has #{count} elements" do
expect(enumerable.count).to eq(count)
end
end
describe Array do
let(:enumerable) { [1,2,3] }
it_behaves_like 'an enumerable', 3
end
模拟与桩:隔离外部依赖
测试替身(Test Doubles)让单元测试不依赖真实的对象、网络或数据库。
创建 double
let(:payment_gateway) { double("PaymentGateway") }
允许方法并指定返回值(stub/allow)
allow(payment_gateway).to receive(:charge).and_return(true)
# 或者一次性设置多个
allow(payment_gateway).to receive_messages(charge: true, refund: false)
期望消息接收(expect/assert)
expect(payment_gateway).to receive(:charge).with(100).and_return(true)
如果方法在示例结束时没有被调用,测试会失败,这是行为验证的核心。
用 instance_double 进行类型检查
instance_double 可基于真实类验证方法签名,防止 stub 不存在的方法。
class Gateway
def charge(amount); end
end
gateway = instance_double("Gateway", charge: true)
gateway.charge(50) # 通过
gateway.invalid_method # 抛出异常,避免 mock 了不存在的方法
class_double 用于类方法
config = class_double("AppConfig").as_stubbed_const(transfer_nested_constants: true)
allow(config).to receive(:app_name).and_return("MyApp")
与 spy 配合(逆向验证)
spy = spy("Listener")
spy.notify("event")
expect(spy).to have_received(:notify).with("event") # 在调用后验证
组织大型测试套件的高级技巧
合理嵌套与上下文分组
避免过度嵌套,通常不超过 3 层。用 describe 分离方法或功能模块,用 context 区分状态。
使用标签过滤
在 .rspec 中可设置默认排除慢测试,或在命令行指定标签:
it "sends email", :slow do
# ...
end
运行 rspec --tag slow 或 rspec --tag ~slow 执行或排除标记的示例。
RSpec.configure 全局设置
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true # 避免 stub 不存在的方法
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus # 允许 fdescribe / fit 聚焦运行
end
自定义匹配器(提升表达力)
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end
expect(10).to be_a_multiple_of(5)