发布于 2026-01-06 0 阅读
0

RSpec 最佳实践

RSpec 最佳实践

我日常工作中会用到 RSpec。它真的非常有用,有了完善的测试用例,你的工作效率会大大提升,这一点怎么强调都不为过。但它出色的灵活性也导致了很多问题,比如测试用例运行速度极慢、过于臃肿,甚至有时难以阅读。我在这里不打算教你 BDD 和 RSpec,而是想分享一些提升测试用例质量和提高 BDD 工作流程效率的方法。

以下是我们在Mekari编写测试代码的一些技巧。这些技巧应该可以帮助您保持测试代码结构良好、易于阅读和理解,并遵循 DRY(Don't Repeat Yourself,不要重复自己)原则。

1. 描述

务必明确描述你所描述的方法。例如,.引用类方法名时,请使用 Ruby 文档的命名约定;#引用实例方法名时,请使用 `this`。

### Bad example
describe 'the authenticate method for User' do
describe 'if the user is an admin' do

### Good example
describe '.authenticate' do
describe '#admin?' do

2. 背景

context以“with”或“when”开头,例如“when status is pending”。

describe '#status_badge' do
  context 'returns css class based on status' do
    context 'when status is pending' do
      let(:request_status) { 'pending' }

      it 'returns css for grey badge' do
        expect(subject.status_badge).to eql 'c-badge--grey'
      end
    end

    context 'when status is approved' do
      let(:request_status) { 'approved' }

      it 'returns css for green badge' do
        expect(subject.status_badge).to eql 'c-badge--green'
      end
    end
  end
end

3. 它

it描述一个测试用例(一个预期结果),并且只指定一个行为。同一个示例中出现多个预期结果,表明你可能在指定多个行为。只指定一个预期结果,有助于你快速找到潜在错误,直接跳转到失败的测试用例,并使你的代码更易读。

总之,在非隔离测试中(例如与数据库、外部 Web 服务集成或端到端测试),为了在每个测试中设置不同的预期结果而反复进行相同的设置,会造成巨大的性能损失。对于这类速度较慢的测试,我认为可以指定多个隔离的行为。

it { is_expected.to belong_to(:job_title).class_name('JobTitle').optional }
it { is_expected.to belong_to(:template).class_name('Approval::Template') }

# or

it 'has relations' do
  is_expected.to belong_to(:job_title).class_name('JobTitle').optional
  is_expected.to belong_to(:template).class_name('Approval::Template')
end

4. 主题

subject它能更清晰地展现你实际测试的内容,并帮助你在测试中保持 DRY(Don't Repeat Yourself,避免重复代码)。

describe Book do
  describe '#valid_isbn?' do
    subject { Book.new(isbn: isbn).valid_isbn? }

    context 'with a valid ISBN number' do
      let(:isbn) { 'valid' }

      # ...
    end

    context 'with an invalid ISBN number' do
      let(:isbn) { 'invalid' }

      # ...
    end
  end  
end

RSpec 的功能远不止这些,你可以在betterspecs 网站上了解更多信息。

5. 嘲讽

mocking这很有趣,通常当我们想要测试的场景需要另一个服务时,我们会进行模拟。

你可以模拟所有内容,这样你的测试用例就不会访问数据库或其他服务。但是this is something wrong,当你的模型代码发生更改,或者你调用的服务的初始化方法发生更改时,你的代码就会出错,而你却无法在合并到生产环境之前获得失败的测试用例。

通过预先模拟对象,您可以专注于当前正在处理的工作。假设您正在开发系统的一个新部分,并且意识到您当前描述和实现的代码需要两个新的协作对象。使用模拟对象,您可以在编写当前代码的规范时定义它们的接口。

这样,在着手实现协作对象之前,确保所有测试都通过,就能维持一个干净的环境。如果没有模拟对象,你就必须在测试通过之前就立即开始编写协作对象的实现代码。这会分散注意力,并可能导致糟糕的代码设计决策。模拟对象通过减少我们一时需要记住的信息量来帮助我们。

既然你可以使用模拟,请记住不要为了让测试用例通过而模拟所有内容,或者让你的测试用例根本不访问数据库。这样做是不对的。当你的对象代码发生更改,或者你调用的对象的初始化方法发生更改时,你的代码会在合并到生产环境之前出现问题,即使没有通过测试用例。

class OvertimeRequest do
  #...
  def allow?
    current_company.active_subscribe?
  end
end

RSpec.describe OvertimeRequest do
  before do
    allow_any_instance_of(Company).to receive(:active_subscribe?).and_return true
  end

  it ... do
  #..
end

6. 自定义匹配器

RSpec 有很多实用的匹配器,我们已经用到了 `matchers` be truebe false`matchers` 等等。有时,当需要期望给定值时,我们会一遍又一遍地重复相同的代码。让我们来看下面的例子。

RSpec.feature 'some feature', type: :feature do
  it 'saves data' do
    #..

    expect(page).to have_css('.c-alert--success', text: 'Saved successfully', visible: :all)
  end

  it 'returns errors' do
    #..

    expect(page).to have_css('.c-alert--failed', text: 'Failed to save', visible: :all)
  end
end

# -- another 100 example to check flash message

如果你能只写这些,那就不会更容易了。

expect(page).to have_flash_message("Saved successfully", type: :success)

如果整个应用程序的逻辑保持一致,最好创建自定义匹配器。要创建这样的匹配器,您需要matchers.rb在目录中创建一个文件(名称随意)spec/support,并将匹配器定义放在其中:

RSpec::Matchers.define :have_flash_message do |message, opts = {}|
  match do |container|
    css_class = opts[:type].present? ? ".c-alert--#{opts[:type]}" : ".c-alert"

    expect(page).to have_css(
      css_class,
      text: message,
      visible: :all
    )
  end
end

最后一步是require 'support/matchers'spec_helper.rb文件中包含匹配器,以引入匹配器。

资源

一些可能有帮助的链接

文章来源:https://dev.to/bambangsinaga/rspec-best-practice-26k5