简介
榜样很重要。
本 RSpec 风格指南概述了现实世界程序员编写可供其他现实世界程序员维护的代码的推荐最佳实践。
RuboCop,一个静态代码分析器(linter)和格式化程序,有一个 rubocop-rspec
扩展,提供了一种方式来强制执行本指南中概述的规则。
注意
|
本指南假设您使用的是 RSpec 3 或更高版本。 |
您可以使用 AsciiDoctor PDF 生成本指南的 PDF 版本,以及使用 AsciiDoctor 生成 HTML 版本 with,使用以下命令
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
提示
|
安装
|
如何阅读本指南
本指南根据整个规范文件的不同部分进行划分。我们试图省略所有显而易见的信息,如果任何内容不清楚,请随时打开一个问题以寻求进一步的说明。
活文档
正如上面的评论所说,本指南仍在不断完善 - 一些规则只是缺乏全面的示例,但 RSpec 世界中的一些东西每周或每月都在变化。也就是说,随着标准的改变,本指南也应该随之改变。
布局
示例组内的空行
不要在 feature
、context
或 describe
描述之后留空行。这不会使代码更易读,并降低了逻辑块的价值。
# bad
describe Article do
describe '#summary' do
context 'when there is a summary' do
it 'returns the summary' do
# ...
end
end
end
end
# good
describe Article do
describe '#summary' do
context 'when there is a summary' do
it 'returns the summary' do
# ...
end
end
end
end
示例组之间的空行
在 feature
、context
或 describe
块之间留一个空行。不要在组中最后一个此类块之后留空行。
# bad
describe Article do
describe '#summary' do
context 'when there is a summary' do
# ...
end
context 'when there is no summary' do
# ...
end
end
describe '#comments' do
# ...
end
end
# good
describe Article do
describe '#summary' do
context 'when there is a summary' do
# ...
end
context 'when there is no summary' do
# ...
end
end
describe '#comments' do
# ...
end
end
let
之后的空行
在 let
、subject
和 before
/after
块之后留一个空行。
# bad
describe Article do
subject { FactoryBot.create(:some_article) }
describe '#summary' do
# ...
end
end
# good
describe Article do
subject { FactoryBot.create(:some_article) }
describe '#summary' do
# ...
end
end
Let 分组
仅将 let
、subject
块分组,并将它们与 before
/after
块分开。这使得代码更易读。
# bad
describe Article do
subject { FactoryBot.create(:some_article) }
let(:user) { FactoryBot.create(:user) }
before do
# ...
end
after do
# ...
end
describe '#summary' do
# ...
end
end
# good
describe Article do
subject { FactoryBot.create(:some_article) }
let(:user) { FactoryBot.create(:user) }
before do
# ...
end
after do
# ...
end
describe '#summary' do
# ...
end
end
示例周围的空行
在 it
/specify
块周围留一个空行。这有助于将期望与它们的条件逻辑(例如上下文)分开。
# bad
describe '#summary' do
let(:item) { double('something') }
it 'returns the summary' do
# ...
end
it 'does something else' do
# ...
end
it 'does another thing' do
# ...
end
end
# good
describe '#summary' do
let(:item) { double('something') }
it 'returns the summary' do
# ...
end
it 'does something else' do
# ...
end
it 'does another thing' do
# ...
end
end
示例组结构
前导 subject
当使用 subject
时,它应该是示例组中的第一个声明。
# bad
describe Article do
before do
# ...
end
let(:user) { FactoryBot.create(:user) }
subject { FactoryBot.create(:some_article) }
describe '#summary' do
# ...
end
end
# good
describe Article do
subject { FactoryBot.create(:some_article) }
let(:user) { FactoryBot.create(:user) }
before do
# ...
end
describe '#summary' do
# ...
end
end
声明 subject
、let!
/let
和 before
/after
钩子
声明 subject
、let!
/let
和 before
/after
钩子时,应按以下顺序进行
-
subject
-
let!
/let
-
before
/after
# bad
describe Article do
before do
# ...
end
after do
# ...
end
let(:user) { FactoryBot.create(:user) }
subject { FactoryBot.create(:some_article) }
describe '#summary' do
# ...
end
end
# good
describe Article do
subject { FactoryBot.create(:some_article) }
let(:user) { FactoryBot.create(:user) }
before do
# ...
end
after do
# ...
end
describe '#summary' do
# ...
end
end
使用上下文
使用上下文使测试清晰、井井有条且易于阅读。
# bad
it 'has 200 status code if logged in' do
expect(response).to respond_with 200
end
it 'has 401 status code if not logged in' do
expect(response).to respond_with 401
end
# good
context 'when logged in' do
it { is_expected.to respond_with 200 }
end
context 'when logged out' do
it { is_expected.to respond_with 401 }
end
上下文案例
context
块几乎总是应该有一个相反的负面案例。如果只有一个上下文(没有匹配的负面案例),则这是一个代码异味,并且此代码需要重构,或者可能没有目的。
# bad - needs refactoring
describe '#attributes' do
context 'the returned hash' do
it 'includes the display name' do
# ...
end
it 'includes the creation time' do
# ...
end
end
end
# bad - the negative case needs to be tested, but isn't
describe '#attributes' do
context 'when display name is present' do
before do
article.display_name = 'something'
end
it 'includes the display name' do
# ...
end
end
end
# good
describe '#attributes' do
subject(:attributes) { article.attributes }
let(:article) { FactoryBot.create(:article) }
context 'when display name is present' do
before do
article.display_name = 'something'
end
it { is_expected.to include(display_name: article.display_name) }
end
context 'when display name is not present' do
before do
article.display_name = nil
end
it { is_expected.not_to include(:display_name) }
end
end
let
块
将 let
和 let!
用于在示例组中的多个示例中使用的數據。使用 let!
定义变量,即使它们在某些示例中没有被引用,例如在测试平衡负面案例时。不要过度使用 let
来定义原始数据,在使用频率和定义复杂度之间找到平衡。
# bad
it 'finds shortest path' do
tree = Tree.new(1 => 2, 2 => 3, 2 => 6, 3 => 4, 4 => 5, 5 => 6)
expect(dijkstra.shortest_path(tree, from: 1, to: 6)).to eq([1, 2, 6])
end
it 'finds longest path' do
tree = Tree.new(1 => 2, 2 => 3, 2 => 6, 3 => 4, 4 => 5, 5 => 6)
expect(dijkstra.longest_path(tree, from: 1, to: 6)).to eq([1, 2, 3, 4, 5, 6])
end
# good
let(:tree) { Tree.new(1 => 2, 2 => 3, 2 => 6, 3 => 4, 4 => 5, 5 => 6) }
it 'finds shortest path' do
expect(dijkstra.shortest_path(tree, from: 1, to: 6)).to eq([1, 2, 6])
end
it 'finds longest path' do
expect(dijkstra.longest_path(tree, from: 1, to: 6)).to eq([1, 2, 3, 4, 5, 6])
end
实例变量
使用 let
定义而不是实例变量。
# bad
before { @name = 'John Wayne' }
it 'reverses a name' do
expect(reverser.reverse(@name)).to eq('enyaW nhoJ')
end
# good
let(:name) { 'John Wayne' }
it 'reverses a name' do
expect(reverser.reverse(name)).to eq('enyaW nhoJ')
end
共享示例
使用共享示例来减少代码重复。
# bad
describe 'GET /articles' do
let(:article) { FactoryBot.create(:article, owner: owner) }
before { page.driver.get '/articles' }
context 'when user is the owner' do
let(:user) { owner }
it 'shows all owned articles' do
expect(page.status_code).to be(200)
contains_resource resource
end
end
context 'when user is an admin' do
let(:user) { FactoryBot.create(:user, :admin) }
it 'shows all resources' do
expect(page.status_code).to be(200)
contains_resource resource
end
end
end
# good
describe 'GET /articles' do
let(:article) { FactoryBot.create(:article, owner: owner) }
before { page.driver.get '/articles' }
shared_examples 'shows articles' do
it 'shows all related articles' do
expect(page.status_code).to be(200)
contains_resource resource
end
end
context 'when user is the owner' do
let(:user) { owner }
include_examples 'shows articles'
end
context 'when user is an admin' do
let(:user) { FactoryBot.create(:user, :admin) }
include_examples 'shows articles'
end
end
# good
describe 'GET /devices' do
let(:resource) { FactoryBot.create(:device, created_from: user) }
it_behaves_like 'a listable resource'
it_behaves_like 'a paginable resource'
it_behaves_like 'a searchable resource'
it_behaves_like 'a filterable list'
end
冗余的 before(:each)
不要为 before
/after
/around
块指定 :each
/:example
范围,因为这是默认值。明确指定范围时,优先使用 :example
。
# bad
describe '#summary' do
before(:example) do
# ...
end
# ...
end
# good
describe '#summary' do
before do
# ...
end
# ...
end
不明确的 Hook 范围
在 before
/after
钩子中使用 :context
代替不明确的 :all
范围。
# bad
describe '#summary' do
before(:all) do
# ...
end
# ...
end
# good
describe '#summary' do
before(:context) do
# ...
end
# ...
end
避免使用 :context
范围的 Hook
避免使用 :context
范围的 before
/after
。注意示例之间状态泄漏的问题。
示例结构
每个示例一个预期值
对于示例,两种风格都被认为是可以接受的。第一种变体是为每个预期值设置单独的示例,这会导致重复的上下文初始化成本。第二种变体是每个示例包含多个预期值,并为组或示例设置 aggregate_failures
标签。在每种情况下,请根据您的判断做出最佳选择,并始终如一地应用您的策略。
# good - one expectation per example
describe ArticlesController do
#...
describe 'GET new' do
it 'assigns a new article' do
get :new
expect(assigns[:article]).to be_a(Article)
end
it 'renders the new article template' do
get :new
expect(response).to render_template :new
end
end
end
# good - multiple expectations with aggregated failures
describe ArticlesController do
#...
describe 'GET new', :aggregate_failures do
it 'assigns new article and renders the new article template' do
get :new
expect(assigns[:article]).to be_a(Article)
expect(response).to render_template :new
end
end
# ...
end
主题
当多个测试与同一个主题相关时,使用 subject
来减少重复。
# bad
it { expect(hero.equipment).to be_heavy }
it { expect(hero.equipment).to include 'sword' }
# good
subject(:equipment) { hero.equipment }
it { expect(equipment).to be_heavy }
it { expect(equipment).to include 'sword' }
命名主题
尽可能使用命名 subject
。仅当您在任何测试中都没有引用它时才使用匿名主题声明,例如,当使用 is_expected
时。
# bad
describe Article do
subject { FactoryBot.create(:article) }
it 'is not published on creation' do
expect(subject).not_to be_published
end
end
# good
describe Article do
subject { FactoryBot.create(:article) }
it 'is not published on creation' do
is_expected.not_to be_published
end
end
# even better
describe Article do
subject(:article) { FactoryBot.create(:article) }
it 'is not published on creation' do
expect(article).not_to be_published
end
end
上下文中的主题命名
当您在不同的上下文中使用不同的属性重新分配主题时,请为主题命名不同的名称,以便更容易地了解实际主题代表什么。
# bad
describe Article do
context 'when there is an author' do
subject(:article) { FactoryBot.create(:article, author: user) }
it 'shows other articles by the same author' do
expect(article.related_stories).to include(story1, story2)
end
end
context 'when the author is anonymous' do
subject(:article) { FactoryBot.create(:article, author: nil) }
it 'matches stories by title' do
expect(article.related_stories).to include(story3, story4)
end
end
end
# good
describe Article do
context 'when article has an author' do
subject(:article) { FactoryBot.create(:article, author: user) }
it 'shows other articles by the same author' do
expect(article.related_stories).to include(story1, story2)
end
end
context 'when the author is anonymous' do
subject(:guest_article) { FactoryBot.create(:article, author: nil) }
it 'matches stories by title' do
expect(guest_article.related_stories).to include(story3, story4)
end
end
end
不要模拟主题
不要模拟被测对象的的方法,这是一种代码异味,通常表明对象的自身设计存在问题。
# bad
describe 'Article' do
subject(:article) { Article.new }
it 'indicates that the author is unknown' do
allow(article).to receive(:author).and_return(nil)
expect(article.description).to include('by an unknown author')
end
end
# good - with correct subject initialization
describe 'Article' do
subject(:article) { Article.new(author: nil) }
it 'indicates that the author is unknown' do
expect(article.description).to include('by an unknown author')
end
end
# good - with better object design
describe 'Article' do
subject(:presenter) { ArticlePresenter.new(article) }
let(:article) { Article.new }
it 'indicates that the author is unknown' do
allow(article).to receive(:author).and_return(nil)
expect(presenter.description).to include('by an unknown author')
end
end
it
和 specify
如果示例没有描述,请使用 specify
,如果示例有描述,请使用 it
。单行示例是一个例外,在这种情况下,it
更可取。当文档字符串不能很好地从 it
中读取时,specify
也很有用。
# bad
it do
# ...
end
specify 'it sends an email' do
# ...
end
specify { is_expected.to be_truthy }
it '#do_something is deprecated' do
...
end
# good
specify do
# ...
end
it 'sends an email' do
# ...
end
it { is_expected.to be_truthy }
specify '#do_something is deprecated' do
...
end
it
在迭代器中
不要编写迭代器来生成测试。当另一个开发人员向迭代中的某个项目添加功能时,他们必须将其分解为单独的测试 - 他们被迫编辑与他们的拉取请求无关的代码。
# bad
[:new, :show, :index].each do |action|
it 'returns 200' do
get action
expect(response).to be_ok
end
end
# good - more verbose, but better for the future development
describe 'GET new' do
it 'returns 200' do
get :new
expect(response).to be_ok
end
end
describe 'GET show' do
it 'returns 200' do
get :show
expect(response).to be_ok
end
end
describe 'GET index' do
it 'returns 200' do
get :index
expect(response).to be_ok
end
end
偶然状态
尽可能避免偶然状态。
# bad
it 'publishes the article' do
article.publish
# Creating another shared Article test object above would cause this
# test to break
expect(Article.count).to eq(2)
end
# good
it 'publishes the article' do
expect { article.publish }.to change(Article, :count).by(1)
end
DRY
小心不要过早地将重复的期望移动到共享环境中,以专注于“DRY”,因为这会导致过于依赖彼此的脆弱测试。
通常,最好从直接在您的 it
块中执行所有操作开始,即使是重复操作,然后在测试正常工作后重构您的测试,使其更 DRY。但是,请记住,测试套件中的重复并不令人反感,事实上,如果它能提供更轻松的理解和阅读测试,它是首选的。
工厂
使用 Factory Bot 在集成测试中创建测试数据。您应该很少需要在集成规范中使用 ModelName.create
。不要使用夹具,因为它们不像工厂那样易于维护。
# bad
subject(:article) do
Article.create(
title: 'Piccolina',
author: 'John Archer',
published_at: '17 August 2172',
approved: true
)
end
# good
subject(:article) { FactoryBot.create(:article) }
注意
|
在谈论单元测试时,最佳实践是不使用夹具或工厂。将尽可能多的域逻辑放在库中,这些库可以在没有工厂或夹具的复杂、耗时的设置的情况下进行测试。 |
所需数据
不要加载超过测试代码所需的数据。
# good
RSpec.describe User do
describe ".top" do
subject { described_class.top(2) }
before { FactoryBot.create_list(:user, 3) }
it { is_expected.to have(2).items }
end
end
双重
优先使用验证双重而不是普通双重。
验证双重是普通双重的更严格的替代方案,它提供保证,例如,如果正在存根无效方法或方法被调用时参数数量无效,则会触发失败。
通常,将双重与更隔离/行为测试一起使用,而不是与集成测试一起使用。
注意
|
没有理由关闭 verify_partial_doubles 配置选项。这将大大降低对部分双重的信心。
|
# good - verifying instance double
article = instance_double('Article')
allow(article).to receive(:author).and_return(nil)
presenter = described_class.new(article)
expect(presenter.title).to include('by an unknown author')
# good - verifying object double
article = object_double(Article.new, valid?: true)
expect(article.save).to be true
# good - verifying partial double
allow(Article).to receive(:find).with(5).and_return(article)
# good - verifying class double
notifier = class_double('Notifier')
expect(notifier).to receive(:notify).with('suspended as')
注意
|
如果您存根了一个可能导致假阳性测试结果的方法,那么您走得太远了。 |
处理时间
始终使用 Timecop,而不是存根 Time 或 Date 上的任何内容。
describe InvoiceReminder do
subject(:time_with_offset) { described_class.new.get_offset_time }
# bad
it 'offsets the time 2 days into the future' do
current_time = Time.now
allow(Time).to receive(:now).and_return(current_time)
expect(time_with_offset).to eq(current_time + 2.days)
end
# good
it 'offsets the time 2 days into the future' do
Timecop.freeze(Time.now) do
expect(time_with_offset).to eq 2.days.from_now
end
end
end
存根 HTTP 请求
当代码正在发出 HTTP 请求时,存根 HTTP 请求。避免访问真实的外部服务。
# good
context 'with unauthorized access' do
let(:uri) { 'http://api.lelylan.com/types' }
before { stub_request(:get, uri).to_return(status: 401, body: fixture('401.json')) }
it 'returns access denied' do
page.driver.get uri
expect(page).to have_content 'Access denied'
end
end
声明常量
不要在示例组中显式声明类、模块或常量。 改为存根常量。
注意
|
常量(包括类和模块)在块作用域中声明时,是在全局命名空间中定义的,并在示例之间泄漏。 |
# bad
describe SomeClass do
CONSTANT_HERE = 'I leak into global namespace'
end
# good
describe SomeClass do
before do
stub_const('CONSTANT_HERE', 'I only exist during this example')
end
end
# bad
describe SomeClass do
class FooClass < described_class
def double_that
some_base_method * 2
end
end
it { expect(FooClass.new.double_that).to eq(4) }
end
# good - anonymous class, no constant needs to be defined
describe SomeClass do
let(:foo_class) do
Class.new(described_class) do
def double_that
some_base_method * 2
end
end
end
it { expect(foo_class.new.double_that).to eq(4) }
end
# good - constant is stubbed
describe SomeClass do
before do
foo_class = Class.new(described_class) do
def do_something
end
end
stub_const('FooClass', foo_class)
end
it { expect(FooClass.new.double_that).to eq(4) }
end
隐式块期望
避免使用隐式块期望。
# bad
subject { -> { do_something } }
it { is_expected.to change(something).to(new_value) }
# good
it 'changes something to a new value' do
expect { do_something }.to change(something).to(new_value)
end
命名
上下文描述
上下文描述应该描述所有示例共享的条件。完整的示例名称(由所有嵌套块描述的串联形成)应该形成一个可读的句子。
典型的描述将是一个附加短语,以“when”、“with”、“without”或类似词语开头。
# bad - 'Summary user logged in no display name shows a placeholder'
describe 'Summary' do
context 'user logged in' do
context 'no display name' do
it 'shows a placeholder' do
end
end
end
end
# good - 'Summary when the user is logged in when the display name is blank shows a placeholder'
describe 'Summary' do
context 'when the user is logged in' do
context 'when the display name is blank' do
it 'shows a placeholder' do
end
end
end
end
示例描述
it
/specify
块描述不应该以条件结尾。这是一个代码异味,表明it
很可能需要包装在context
中。
# bad
it 'returns the display name if it is present' do
# ...
end
# good
context 'when display name is present' do
it 'returns the display name' do
# ...
end
end
# This encourages the addition of negative test cases that might have
# been overlooked
context 'when display name is not present' do
it 'returns nil' do
# ...
end
end
保持示例描述简短
保持示例描述小于 60 个字符。
编写自文档化的示例,并生成正确的文档格式输出。
# bad
it 'rewrites "should not return something" as "does not return something"' do
# ...
end
# good
it 'rewrites "should not return something"' do
expect(rewrite('should not return something')).to
eq 'does not return something'
end
# good - self-documenting
specify do
expect(rewrite('should not return something')).to
eq 'does not return something'
end
示例文档字符串中的“Should”
不要在示例文档字符串的开头写“should”或“should not”。描述代表实际功能,而不是可能发生的事情。使用第三人称现在时。
# bad
it 'should return the summary' do
# ...
end
# good
it 'returns the summary' do
# ...
end
描述方法
清楚地说明您要描述的方法。使用 Ruby 文档约定,在引用类方法名称时使用.
,在引用实例方法名称时使用#
。
# bad
describe 'the authenticate method for User' do
# ...
end
describe 'if the user is an admin' do
# ...
end
# good
describe '.authenticate' do
# ...
end
describe '#admin?' do
# ...
end
使用expect
始终使用较新的expect
语法。
配置 RSpec 仅接受新的expect
语法。
# bad
it 'creates a resource' do
response.should respond_with_content_type(:json)
end
# good
it 'creates a resource' do
expect(response).to respond_with_content_type(:json)
end
匹配器
谓词匹配器
尽可能使用 RSpec 的谓词匹配器方法。
describe Article do
subject(:article) { FactoryBot.create(:article) }
# bad
it 'is published' do
expect(article.published?).to be true
end
# good
it 'is published' do
expect(article).to be_published
end
# even better
it { is_expected.to be_published }
end
内置匹配器
使用内置匹配器。
# bad
it 'includes a title' do
expect(article.title.include?('a lengthy title')).to be true
end
# good
it 'includes a title' do
expect(article.title).to include 'a lengthy title'
end
be
匹配器
避免在没有参数的情况下使用 be
匹配器。它过于通用,因为它会通过所有不是 nil
或 false
的内容。如果这是确切的意图,请使用 be_truthy
。在所有其他情况下,最好指定期望值是什么。
# bad
it 'has author' do
expect(article.author).to be
end
# good
it 'has author' do
expect(article.author).to be_truthy # same as the original
expect(article.author).not_to be_nil # `be` is often used to check for non-nil value
expect(article.author).to be_an(Author) # explicit check for the type of the value
end
将常见的期望部分提取到匹配器中
将示例中经常使用的通用逻辑提取到 自定义匹配器 中。
# bad
it 'returns JSON with temperature in Celsius' do
json = JSON.parse(response.body).with_indifferent_access
expect(json[:celsius]).to eq 30
end
it 'returns JSON with temperature in Fahrenheit' do
json = JSON.parse(response.body).with_indifferent_access
expect(json[:fahrenheit]).to eq 86
end
# good
it 'returns JSON with temperature in Celsius' do
expect(response).to include_json(celsius: 30)
end
it 'returns JSON with temperature in Fahrenheit' do
expect(response).to include_json(fahrenheit: 86)
end
any_instance_of
避免使用 allow_any_instance_of
/expect_any_instance_of
。这可能表明被测对象过于复杂,并且在与接收计数一起使用时会产生歧义。
# bad
it 'has a name' do
allow_any_instance_of(User).to receive(:name).and_return('Tweedledee')
expect(account.name).to eq 'Tweedledee'
end
# good
let(:account) { Account.new(user) }
it 'has a name' do
allow(user).to receive(:name).and_return('Tweedledee')
expect(account.name).to eq 'Tweedledee'
end
匹配器库
使用提供便利帮助程序的第三方匹配器库,这些帮助程序将大大简化示例,Shoulda Matchers 是值得一提的一个。
# bad
describe '#title' do
it 'is required' do
article.title = nil
article.valid?
expect(article.errors[:title])
.to contain_exactly('Article has no title')
not
end
end
# good
describe '#title' do
it 'is required' do
expect(article).to validate_presence_of(:title)
.with_message('Article has no title')
end
end
Rails:集成
测试你所看到的。深入测试你的模型和应用程序行为(集成测试)。不要添加无用的复杂性测试控制器。
这是 Ruby 社区中一个公开的争论,双方都有很好的论据来支持他们的观点。支持测试控制器必要性的人会告诉你,你的集成测试没有涵盖所有用例,而且它们很慢。两者都是错误的。可以涵盖所有用例,也可以让它们变得更快。
Rails:视图
视图目录结构
视图规范的目录结构 spec/views
与 app/views
中的目录结构相匹配。例如,app/views/users
中视图的规范位于 spec/views/users
中。
视图规范文件名
视图规范的命名约定是在视图名称中添加 _spec.rb
,例如视图 _form.html.erb
具有相应的规范 _form.html.erb_spec.rb
。
查看外部 describe
外部 describe
块使用视图的路径,不包含 app/views
部分。这在 render
方法没有参数调用时使用。
# spec/views/articles/new.html.erb_spec.rb
describe 'articles/new.html.erb' do
# ...
end
查看模拟模型
在视图规范中始终模拟模型。视图的唯一目的是显示信息。
视图 assign
assign
方法提供视图使用的实例变量,这些变量由控制器提供。
# spec/views/articles/edit.html.erb_spec.rb
describe 'articles/edit.html.erb' do
it 'renders the form for a new article creation' do
assign(:article, double(Article).as_null_object)
render
expect(rendered).to have_selector('form',
method: 'post',
action: articles_path
) do |form|
expect(form).to have_selector('input', type: 'submit')
end
end
end
Capybara 负选择器
优先使用 Capybara 负选择器,而不是使用 to_not
和正选择器。
# bad
expect(page).to_not have_selector('input', type: 'submit')
expect(page).to_not have_xpath('tr')
# good
expect(page).to have_no_selector('input', type: 'submit')
expect(page).to have_no_xpath('tr')
视图助手存根
当视图使用助手方法时,需要对这些方法进行存根。助手方法的存根是在 template
对象上完成的。
# app/helpers/articles_helper.rb
class ArticlesHelper
def formatted_date(date)
# ...
end
end
# app/views/articles/show.html.erb
<%= 'Published at: #{formatted_date(@article.published_at)}' %>
# spec/views/articles/show.html.erb_spec.rb
describe 'articles/show.html.erb' do
it 'displays the formatted date of article publishing' do
article = double(Article, published_at: Date.new(2012, 01, 01))
assign(:article, article)
allow(template).to_receive(:formatted_date).with(article.published_at).and_return('01.01.2012')
render
expect(rendered).to have_content('Published at: 01.01.2012')
end
end
视图助手
助手规范与 spec/helpers
目录中的视图规范分开。
Rails: 控制器
控制器模型
模拟模型并存根其方法。测试控制器不应该依赖于模型的创建。
控制器行为
只测试控制器应该负责的行为
-
特定方法的执行
-
从操作返回的数据 - 赋值等。
-
操作的结果 - 模板渲染、重定向等。
# Example of a commonly used controller spec
# spec/controllers/articles_controller_spec.rb
# We are interested only in the actions the controller should perform
# So we are mocking the model creation and stubbing its methods
# And we concentrate only on the things the controller should do
describe ArticlesController do
# The model will be used in the specs for all methods of the controller
let(:article) { double(Article) }
describe 'POST create' do
before { allow(Article).to receive(:new).and_return(article) }
it 'creates a new article with the given attributes' do
expect(Article).to receive(:new).with(title: 'The New Article Title').and_return(article)
post :create, message: { title: 'The New Article Title' }
end
it 'saves the article' do
expect(article).to receive(:save)
post :create
end
it 'redirects to the Articles index' do
allow(article).to receive(:save)
post :create
expect(response).to redirect_to(action: 'index')
end
end
end
控制器上下文
当控制器操作根据接收到的参数具有不同的行为时,使用上下文。
# A classic example for use of contexts in a controller spec is creation or update when the object saves successfully or not.
describe ArticlesController do
let(:article) { double(Article) }
describe 'POST create' do
before { allow(Article).to receive(:new).and_return(article) }
it 'creates a new article with the given attributes' do
expect(Article).to receive(:new).with(title: 'The New Article Title').and_return(article)
post :create, article: { title: 'The New Article Title' }
end
it 'saves the article' do
expect(article).to receive(:save)
post :create
end
context 'when the article saves successfully' do
before do
allow(article).to receive(:save).and_return(true)
end
it 'sets a flash[:notice] message' do
post :create
expect(flash[:notice]).to eq('The article was saved successfully.')
end
it 'redirects to the Articles index' do
post :create
expect(response).to redirect_to(action: 'index')
end
end
context 'when the article fails to save' do
before do
allow(article).to receive(:save).and_return(false)
end
it 'assigns @article' do
post :create
expect(assigns[:article]).to eq(article)
end
it "re-renders the 'new' template" do
post :create
expect(response).to render_template('new')
end
end
end
end
Rails: 模型
模型模拟
不要在模型自己的规范中模拟模型。
模型对象
使用 FactoryBot.create
创建真实对象,或者只使用 subject
创建一个新的(未保存的)实例。
describe Article do
subject(:article) { FactoryBot.create(:article) }
it { is_expected.to be_an Article }
it { is_expected.to be_persisted }
end
模型模拟关联
模拟其他模型或子对象是可以接受的。
避免模型测试中的重复
为规范中的所有示例创建模型以避免重复。
describe Article do
let(:article) { FactoryBot.create(:article) }
end
检查模型有效性
添加一个示例以确保使用 FactoryBot.create
创建的模型是有效的。
describe Article do
it 'is valid with valid attributes' do
expect(article).to be_valid
end
end
模型验证
在测试验证时,使用 expect(model.errors[:attribute].size).to eq(x)
指定应验证的属性。使用 be_valid
不能保证问题出在预期的属性上。
# bad
describe '#title' do
it 'is required' do
article.title = nil
expect(article).to_not be_valid
end
end
# preferred
describe '#title' do
it 'is required' do
article.title = nil
article.valid?
expect(article.errors[:title].size).to eq(1)
end
end
为属性验证创建单独的示例组
为每个具有验证的属性添加一个单独的 describe
。
describe '#title' do
it 'is required' do
article.title = nil
article.valid?
expect(article.errors[:title].size).to eq(1)
end
end
describe '#name' do
it 'is required' do
article.name = nil
article.valid?
expect(article.errors[:name].size).to eq(1)
end
end
命名另一个对象
在测试模型属性的唯一性时,将另一个对象命名为 another_object
。
describe Article do
describe '#title' do
it 'is unique' do
another_article = FactoryBot.create(:article, title: article.title)
article.valid?
expect(article.errors[:title].size).to eq(1)
end
end
end
Rails:邮件器
邮件器模拟模型
邮件器规范中的模型应该被模拟。邮件器不应该依赖于模型的创建。
邮件器期望
邮件器规范应该验证
-
主题是否正确
-
发件人电子邮件是否正确
-
电子邮件是否发送到正确的收件人
-
电子邮件是否包含所需的信息
describe SubscriberMailer do
let(:subscriber) { double(Subscription, email: '[email protected]', name: 'John Doe') }
describe 'successful registration email' do
subject(:email) { SubscriptionMailer.successful_registration_email(subscriber) }
it { is_expected.to have_attributes(subject: 'Successful Registration!', from: ['infor@your_site.com'], to: [subscriber.email]) }
it 'contains the subscriber name' do
expect(email.body.encoded).to match(subscriber.name)
end
end
end
建议
正确设置
正确设置全局 RSpec 配置(~/.rspec
)、每个项目(.rspec
)以及项目覆盖文件(.rspec-local
),该文件应该被保留在版本控制之外。使用 rspec --init
生成 .rspec
和 spec/spec_helper.rb
文件。
# .rspec --color --require spec_helper # .rspec-local --profile 2
许可证
本作品根据 知识共享署名 3.0 通用许可协议 授权
鸣谢
灵感来自以下内容
本指南由 ReachLocal 长期维护。
本指南包含最初存在于 BetterSpecs (较新网站 较旧网站) 中的材料,由 Lelylan 赞助,由 Andrea Reginato 和 许多其他人 长期维护。