RSpec 核心概念

FreeGuideOnline 最新 2026-07-14

bash gem install rspec


或者将 `rspec` 添加到 Gemfile:

```ruby
group :test do
  gem 'rspec', '~> 3.12'
end

然后运行:

bundle install

初始化 RSpec 配置(生成 spec 目录和辅助文件):

rspec --init

这会创建 .rspecspec/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 使用 describecontextit 来组织测试代码,形成清晰的结构层次,同时也生成可读的文档字符串。

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_truthybe_falseybe_nil
  • 比较匹配be >be >=be <be <=
  • 类型检查be_a(Class) / be_an_instance_of
  • 谓词匹配器:有以 be_ 开头的动态方法,如 be_valid,对应 valid? 方法
  • 集合匹配includematch_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) 中不能使用实例变量在示例间共享(因为示例可能在不同顺序下执行),一般配合 letclass 变量使用较少。

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_returnand_raiseand_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 排除它们。

焦点测试

在开发时,可以使用 fdescribefcontextfit 只运行特定块,或使用 :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_arraycontain_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)