RSpec 核心概念
bash gem install rspec
或者将 `rspec` 添加到 Gemfile:
```ruby
group :test do
gem 'rspec', '~> 3.12'
end
然后运行:
bundle install
初始化 RSpec 配置(生成 spec 目录和辅助文件):
rspec --init
这会创建 .rspec 和 spec/spec_helper.rb 两个文件。你可以在 .rspec 中加入 --format documentation 来获得更友好的输出格式。
第一个测试文件
创建文件 spec/hello_spec.rb:
RSpec.describe 'Hello World' do
it 'returns a greeting' do
expect('Hello World').to eq('Hello World')
end
end
运行 rspec,你将看到绿色通过。接下来我们深入核心构造块。
核心构造块:describe / context / it
RSpec 使用 describe、context 和 it 来组织测试代码,形成清晰的结构层次,同时也生成可读的文档字符串。
describe
describe 顶层块通常描述被测试的单元,可以是一个类、一个方法或一个功能。它接受一个描述字符串和可选的模块/类参数(使得内部可以直接引用常量)。
RSpec.describe Calculator, '#add' do
# ...
end
如果不传类参数,直接用字符串描述也可以。在块内部,described_class 方法会返回传入的类(如果有的话),这对重构非常友好。
context
context 其实是 describe 的别名,但它通常用于区分不同的场景或条件,使测试更具业务含义。
RSpec.describe '#age_group' do
context 'when age < 18' do
it { expect(...).to eq 'minor' }
end
context 'when age >= 18' do
it { expect(...).to eq 'adult' }
end
end
it
it 用于定义一个具体的测试示例(example)。块内包含实际的断言逻辑。it 后面的字符串应与所有外层描述拼接成一个完整的句子,例如:
RSpec.describe '#sum' do
it 'returns the sum of two numbers'
end
输出文档格式时会显示:#sum returns the sum of two numbers。如果暂时没有实现,可以省略块,此时示例会被标记为 pending。
期望与匹配器
RSpec 的核心验证通过 expect 函数产生的期望对象调用匹配器来完成。
基本语法
expect(actual).to matcher(expected)
expect(actual).not_to matcher(expected) # 或 .to_not
常用匹配器
- 值相等:
eq(等同于==)、eql(等同于eql?)、equal(对象身份相同) - 真值检查:
be_truthy、be_falsey、be_nil - 比较匹配:
be >、be >=、be <、be <= - 类型检查:
be_a(Class)/be_an_instance_of - 谓词匹配器:有以
be_开头的动态方法,如be_valid,对应valid?方法 - 集合匹配:
include、match_array(数组元素匹配,不关心顺序)、contain_exactly(同 match_array) - 正则匹配:
match(/pattern/) - 响应方法:
respond_to(:method) - 变化匹配:
expect { ... }.to change(Model, :count).by(1) - 错误抛出:
expect { ... }.to raise_error(ErrorClass, /message regex/)
示例:
expect(10 / 2).to eq(5)
expect([1, 2, 3]).to include(2)
expect(obj).to be_valid
expect { obj.update!(title: nil) }.to raise_error(ActiveRecord::RecordInvalid)
组合匹配器
可以使用 and / or 组合多个匹配器:
expect('hello').to start_with('h').and end_with('o')
expect('hello').to end_with('o').or eq('hi')
测试辅助:let、let!、subject
为了避免重复创建对象,RSpec 提供了懒惰加载和显式声明对象的方法。
let(懒加载)
let 定义一个记忆化的辅助方法,只在第一次调用时求值,并在每个示例之间缓存。
RSpec.describe User do
let(:user) { User.new(name: 'Alice') }
it 'has a name' do
expect(user.name).to eq('Alice')
end
end
如果在示例中没有调用 user,block 永远不会执行。
let!(立即加载)
let! 会在每个示例前立刻执行块,即使示例中没有显式引用它。常用于创建需要持久化到数据库的对象(例如在 Rails 中创建工厂记录)。
let!(:admin) { create(:user, role: :admin) }
it 'has an admin user' do
expect(User.admin.count).to eq(1)
end
subject
subject 是 RSpec 中隐式或显式定义的主题对象。你可以使用 is_expected 语法来简化单行期望。
显式定义:
RSpec.describe Calculator do
subject(:calc) { Calculator.new }
it 'starts at zero' do
expect(calc.result).to eq(0)
end
end
隐式主题(使用 describe 块内类构造):
RSpec.describe Calculator do
it { is_expected.to be_a(Calculator) }
end
在 Rails 模型测试中常配合 expect(subject) 或 is_expected 进行验证。
钩子:before / after / around
钩子用于控制测试执行的准备和清理操作。
before / after
before(:each) 每个示例执行前运行(默认),before(:all) 整个 describe/context 中所有示例执行前只运行一次。
RSpec.describe 'Order processing' do
before(:each) do
@order = Order.create(total: 100)
end
after(:each) do
@order.destroy
end
it 'calculates tax' do
expect(@order.tax).to eq(10)
end
end
注意:before(:all) 中不能使用实例变量在示例间共享(因为示例可能在不同顺序下执行),一般配合 let 或 class 变量使用较少。
around
around 围绕示例执行,你可以手动的调用示例。常用于数据库事务包裹或资源管理。
around(:each) do |example|
DB.transaction(rollback: :always) { example.run }
end
共享示例与上下文
当多个 describe 块中有相同的测试逻辑时,可以使用 shared_examples 来消除重复。
shared_examples
shared_examples 'a sortable collection' do
it 'responds to sort_by' do
expect(subject).to respond_to(:sort_by)
end
end
RSpec.describe Array do
it_behaves_like 'a sortable collection'
end
RSpec.describe Set do
it_behaves_like 'a sortable collection'
end
你也可以向共享示例传递参数:
shared_examples 'a printable' do |format|
it "outputs as #{format}" do
expect(subject.to_s).to be_a(String)
end
end
RSpec.describe Invoice do
it_behaves_like 'a printable', :pdf
end
shared_context
如果共享的是辅助方法、let 或钩子,可以使用 shared_context:
shared_context 'logged in user' do
let(:current_user) { create(:user) }
before { login_as current_user }
end
RSpec.describe DashboardController do
include_context 'logged in user'
it 'shows the dashboard' do
get :show
expect(response).to have_http_status(:ok)
end
end
测试双(Test Doubles)与消息期望
RSpec 提供强大的模拟(mocks)和桩(stubs)功能,用于隔离测试。
双对象 double
let(:mailer) { double('Mailer') }
你可以让双对象模拟方法返回值或验证方法是否被调用。
桩(Stubbing)
allow(double).to receive(:send).and_return(true)
对一个真实对象也可以:
allow_any_instance_of(User).to receive(:save).and_return(false)
消息期望(Message Expectations)
验证某个方法被调用:
expect(double).to receive(:notify).with('Success').once
结合 and_return、and_raise、and_call_original 等。
部分双与测试间谍
RSpec 还支持 spy 模式,先定义桩,后验证:
spy = spy('Observer')
subject.save
expect(spy).to have_received(:after_save).with(subject)
标签、过滤与焦点测试
标签
你可以给示例或上下文打标签:
RSpec.describe 'Heavy test', :slow do
it 'does something big', :visual do
# ...
end
end
然后使用 rspec --tag slow 只运行带 slow 标签的测试,或用 --tag ~slow 排除它们。
焦点测试
在开发时,可以使用 fdescribe、fcontext、fit 只运行特定块,或使用 :focus 标签,需要配置:
# spec_helper.rb
config.filter_run_when_matching :focus
然后:
fit 'only this runs' do ... end
常用高级匹配器与自定义匹配器
属性匹配器 have_attributes
expect(user).to have_attributes(name: 'Bob', age: (a_value > 20))
响应匹配器 respond_to 的参数验证
expect(obj).to respond_to(:save!).with(0).arguments
匹配数组包含元素 match_array 与 contain_exactly
expect([1,2,3]).to contain_exactly(3,2,1)
自定义匹配器
通过 DSL 创建可复用的匹配器:
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
end
# 使用
expect(12).to be_a_multiple_of(3)