RSpec 深入解析

FreeGuideOnline 最新 2026-07-14

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 结尾。


测试的基本骨架:describecontextit

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)

测试准备与数据隔离:beforeafterlet

before / after 钩子

在同一个 describecontext 块内,钩子会在每个示例(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(全局)。

letlet!

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 slowrspec --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)