什么是Better Specs
Better Specs 是开发人员在测试应用程序时学到的最佳实践的集合,您可以用它来提高自己的编码技能,或者仅仅从中获得灵感。Better Specs 诞生于 Lelylan(开源物联网云平台),查看其测试套件可能会有所启发。
Better Specs专注于 Rails 测试,但我们的目标是创建涵盖大多数语言和框架(如 Scala、Elixir、React)的测试指南。如果您想添加您最喜欢的语言测试指南,请打开问题。
描述您的方法
明确您要描述的方法。例如,在引用类方法名称时,请使用 Ruby 文档约定的 .(或 :: ),而在引用实例方法名称时,请使用 #。
#坏的------------------------------------------------
describe 'the authenticate method for User' do
describe 'if the user is an admin' do
#好的------------------------------------------------
describe '.authenticate' do
describe '#admin?' do
使用上下文
上下文是使测试清晰、有条理(使测试易于阅读)的有效方法。描述上下文时,应以 "当when"、"有with "或 "无without "开始。
#坏的------------------------------------------------
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
#好的------------------------------------------------
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
简短描述
spec说明不应超过 40 个字符。如果出现这种情况,应使用上下文分割。
#坏的------------------------------------------------
it 'has 422 status code if an unexpected params will be added' do
#好的------------------------------------------------
context 'when not valid' do
it { is_expected.to respond_with 422 }
end
在示例中,我们删除了与状态代码相关的描述,取而代之的是期望值 is_expected。如果键入 rspec filename 运行此测试,将获得可读的输出结果。
#格式化输出------------------------------------------------
when not valid
it should respond with 422
单一期望测试
"一个期待 "提示更广泛地表述为 "每个测试应只做一个断言"。这有助于您发现可能的错误,直接找到失败的测试,并使您的代码具有可读性。在孤立的单元规范中,您希望每个示例指定一个(且只有一个)行为。同一示例中的多个期望是一个信号,表明您可能指定了多个行为。
#好的(隔离)------------------------------------------------
it { is_expected.to respond_with_content_type(:json) }
it { is_expected.to assign_to(:resource) }
总之,在非隔离测试中(如与数据库、外部 Web 服务或端到端测试集成的测试),为了在每个测试中设置不同的期望值而反复进行相同的设置,会对性能造成巨大影响。在这类速度较慢的测试中,我认为可以指定多个孤立的行为。
#好的(不隔离)------------------------------------------------
it 'creates a resource' do
expect(response).to respond_with_content_type(:json)
expect(response).to assign_to(:resource)
end
测试所有可能的情况
测试是一种很好的做法,但如果不测试边缘情况,测试就不会有用。测试有效案例、边缘案例和无效案例。例如,请考虑以下操作。
#销毁操作------------------------------------------------
before_action :find_owned_resources
before_action :find_resource
def destroy
render 'show'
@consumption.destroy
end
我通常看到的错误在于只测试资源是否已被移除。但至少有两种边缘情况:找不到资源和不拥有资源。根据经验,应考虑所有可能的输入并对其进行测试。
#坏的------------------------------------------------
it 'shows the resource'
#好的------------------------------------------------
describe '#destroy' do
context 'when resource is found' do
it 'responds with 200'
it 'shows the resource'
end
context 'when resource is not found' do
it 'responds with 404'
end
context 'when resource is not owned' do
it 'responds with 404'
end
end
Expect与Should语法
在新项目中,始终使用 expect 语法。
#坏的------------------------------------------------
it 'creates a resource' do
response.should respond_with_content_type(:json)
end
#好的------------------------------------------------
it 'creates a resource' do
expect(response).to respond_with_content_type(:json)
end
配置 RSpec,使其仅在新项目中接受新语法,以避免到处都是 2 种语法。
#好的------------------------------------------------
# spec_helper.rb
RSpec.configure do |config|
# ...
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
对于单行期望或隐含主题,我们应该使用 is_expected.to。
#坏的------------------------------------------------
context 'when not valid' do
it { should respond_with 422 }
end
#好的------------------------------------------------
context 'when not valid' do
it { is_expected.to respond_with 422 }
end
对于旧项目,可以使用 transpec 将其转换为新语法。有关新 RSpec 期望语法的更多信息,请参见此处和此处。
使用subject
如果您有多个与同一主题相关的测试,请使用 subject{} 将它们 DRY 起来。
#坏的------------------------------------------------
it { expect(assigns('message')).to match /it was born in Belville/ }
#好的------------------------------------------------
subject { assigns('message') }
it { is_expected.to match /it was born in Billville/ }
RSpec 还能使用命名主题(了解有关 rspec 主题的更多信息)。
#好的------------------------------------------------
subject(:hero) { Hero.first }
it "carries a sword" do
expect(hero.equipment).to include "sword"
end
使用let和let!
当需要给变量赋值而不是使用 before 代码块创建实例变量时,可以使用 let。只有在测试中首次使用let 时,变量才会懒加载,并缓存到特定测试结束。关于 let 的作用,可以在 stackoverflow 答案中找到深入浅出的描述。
#坏的------------------------------------------------
describe '#type_id' do
before { @resource = FactoryBot.create :device }
before { @type = Type.find @resource.type_id }
it 'sets the type_id field' do
expect(@resource.type_id).to eq(@type.id)
end
end
#好的------------------------------------------------
describe '#type_id' do
let(:resource) { FactoryBot.create :device }
let(:type) { Type.find resource.type_id }
it 'sets the type_id field' do
expect(resource.type_id).to eq(type.id)
end
end
使用 let 延迟初始化操作,以测试您的specs。
#好的------------------------------------------------
context 'when updates a not existing property value' do
let(:properties) { { id: Settings.resource_id, value: 'on'} }
def update
resource.properties = properties
end
it 'raises a not found error' do
expect { update }.to raise_error Mongoid::Errors::DocumentNotFound
end
end
如果想在定义块时定义变量,请使用 let!这对于填充数据库以测试查询或作用域非常有用。下面举例说明 let 的实际作用(了解有关 rspec let 的更多信息)。
#解释------------------------------------------------
# this use of let
let(:foo) { Foo.new }
# is very nearly equivalent to this:
def foo
@foo ||= Foo.new
end
使不使用Mock
一般来说,不要(过度)使用Mock,尽可能测试真实行为,因为测试真实案例对验证应用程序流程非常有用。
#好的------------------------------------------------
# simulate a not found resource
context "when not found" do
before do
allow(Resource).to receive(:where).with(created_from: params[:id])
.and_return(false)
end
it { is_expected.to respond_with 404 }
end
模拟可以使您的specs更快,但却很难使用。您需要充分了解它们,才能很好地使用它们。阅读本文,了解有关Mock的更多信息。
只创建所需的数据
如果您曾在一个中等规模的项目中工作过(也曾在小型项目中工作过),那么测试套件的运行可能会很繁重。要解决这个问题,重要的是不要加载超过需要的数据。此外,如果您认为需要几十条记录,那就大错特错了。
#好的------------------------------------------------
describe "User" do
describe ".top" do
before { FactoryBot.create_list(:user, 3) }
it { expect(User.top(2)).to have(2).item }
end
end
使用factories而不是fixtures
这是个老话题了,但还是应该记住。不要使用固定装置,因为它们难以控制,而应使用工厂。使用工厂可以减少创建新数据时的繁琐(了解 Factory Bot)。
#坏的------------------------------------------------
user = User.create(
name: 'Genoveffa',
surname: 'Piccolina',
city: 'Billyville',
birth: '17 Agoust 1982',
active: true
)
#好的
user = FactoryBot.create :user
有一点很重要。在讨论单元测试时,最佳做法是既不使用固定装置,也不使用工厂。将尽可能多的领域逻辑放在可以测试的库中,而不需要使用工厂或固定装置进行复杂、耗时的设置。阅读本文的更多内容。
易读匹配器
使用可读匹配器,并仔细检查可用的 rspec 匹配器。
#坏的------------------------------------------------
lambda { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
#好的------------------------------------------------
expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFound
共享示例
进行测试是件好事,你会一天比一天更有信心。但到头来,你会发现到处都是重复的代码。使用共享示例来提高测试套件的 DRY。
#坏的------------------------------------------------
describe 'GET /devices' do
let!(:resource) { FactoryBot.create :device, created_from: user.id }
let!(:uri) { '/devices' }
context 'when shows all resources' do
let!(:not_owned) { FactoryBot.create factory }
it 'shows all owned resources' do
page.driver.get uri
expect(page.status_code).to be(200)
contains_owned_resource resource
does_not_contain_resource not_owned
end
end
describe '?start=:uri' do
it 'shows the next page' do
page.driver.get uri, start: resource.uri
expect(page.status_code).to be(200)
contains_resource resources.first
expect(page).to_not have_content resource.id.to_s
end
end
end
#好的------------------------------------------------
describe 'GET /devices' do
let!(:resource) { FactoryBot.create :device, created_from: user.id }
let!(:uri) { '/devices' }
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
根据我们的经验,共享示例主要用于控制器。由于模型之间差异很大,它们(通常)不会共享太多逻辑。了解有关 rspec 共享示例的更多信息。
测试您所看到的
深入测试模型和应用程序行为(集成测试)。不要增加测试控制器的无用复杂性。
刚开始测试应用程序时,我测试控制器,现在我不这样做了。现在我只使用 RSpec 和 Capybara 创建集成测试。为什么呢?因为我相信你应该测试你所看到的,而且测试控制器是你通常不需要的额外步骤。你会发现你的大部分测试都会进入模型,而且集成测试可以很容易地归类到共享示例中,从而构建一个清晰可读的测试套件。
这是 Ruby 社区的一场公开辩论,双方都有很好的论据支持自己的观点。支持需要测试控制器的人会告诉你,你的集成测试无法覆盖所有用例,而且速度很慢。这两种说法都是错误的。您可以轻松覆盖所有用例(为什么不呢?),还可以使用 Guard 等自动化工具运行单个文件规格。这样,您只需快速运行需要测试的规范,而无需停止流程。
不使用should
描述测试时不要使用 "应该"。使用现在时第三人称。最好开始使用新的期望语法。
#坏的------------------------------------------------
it 'should not change timings' do
consumption.occur_at.should == valid.occur_at
end
#好的------------------------------------------------
it 'does not change timings' do
expect(consumption.occur_at).to eq(valid.occur_at)
end
请参阅 should_not gem 以了解在 RSpec 中执行此功能的方法,以及 should_clean gem 以了解清理以 "should "开头的现有 RSpec 示例的方法。
用guard自动测试
每次更改应用程序时都要运行所有测试套件会很麻烦。这不仅会耗费大量时间,还会破坏您的流程。有了 Guard,您就可以自动化测试套件,只运行与您正在处理的更新规范、模型、控制器或文件相关的测试。
#好的------------------------------------------------
bundle exec guard
在这里,您可以看到带有一些基本重载规则的 Guardfile 示例。
#好的------------------------------------------------
guard 'rspec', cli: '--drb --format Fuubar --color', version: 2 do
# run every updated spec file
watch(%r{^spec/.+_spec\.rb$})
# run the lib specs when a file in lib/ changes
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
# run the model specs related to the changed model
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
# run the view specs related to the changed view
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
# run the integration specs related to the changed controller
watch(%r{^app/controllers/(.+)\.rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
# run all integration tests when application controller change
watch('app/controllers/application_controller.rb') { "spec/requests" }
end
Guard 是一款优秀的工具,但它并不能满足你的所有需求。有时,你的 TDD 工作流最好使用按键绑定,这样就能在需要时轻松运行你想要的示例。然后,你可以在推送代码前使用 rake 任务运行整个套件。在此查找 vim 按键绑定示例,了解有关 guard-rspec 的更多信息。
更快的测试(预加载 Rails)
在 Rails 上运行测试时,需要加载整个 Rails 应用程序。这可能会耗费时间,并破坏开发流程。要解决这个问题,可以使用 Zeus、Spin 或 Spork 等解决方案。这些解决方案将预加载您(通常)不会更改的所有库,并重新加载控制器、模型、视图、工厂和您最常更改的所有文件。
在这里,你可以找到基于 Spork 的规范助手和 Guardfile 配置。使用该配置,如果预加载文件(如初始化器)发生变化,您将重新加载整个应用程序,而且运行单个测试的速度将非常非常快。
使用 Spork 的缺点是,它会给你的代码打上 "猴子补丁"(monkey-patch),你可能要花上几个小时才能弄明白为什么文件没有被重新加载。如果您有使用 Spin 或其他解决方案的代码示例,请告诉我们。
在这里您可以找到使用 Zeus 的 Guardfile 配置。无需修改 spec_helper,但在运行测试之前,必须在控制台中运行 `zeus start` 以启动 Zeus 服务器。虽然 Zeus 采用的方法没有 Spork 那么激进,但它的一个主要缺点是使用要求相当严格;需要 Ruby 1.9.3 以上(建议使用 Ruby 2.0 的后向 GC)以及支持 FSEvents 或 inotify 的操作系统。
许多批评都是针对这些解决方案的。这些库只是解决问题的 "创可贴",通过更好的设计和有意识地只加载所需的依赖关系才能更好地解决问题。阅读相关讨论,了解更多信息。
存根 HTTP 请求
有时,您需要访问外部服务。在这种情况下,你不能依赖真实的服务,而应该使用 webmock 等解决方案来存根。
#好的------------------------------------------------
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 "gets a not authorized notification" do
page.driver.get uri
expect(page).to have_content 'Access denied'
end
end
进一步了解 webmock 和 VCR。这里有一个很好的演示,解释了如何将它们混合在一起。
有用的格式化
#好的------------------------------------------------
# Gemfile
group :development, :test do
gem 'fuubar'
# .rspec configuration file
--drb
--format Fuubar
--color
了解有关 fuubar 的更多信息。
捐款
"随意提交 PR "是 GitHub 上经常出现的字眼,但很多人对此感到困惑和恐惧。开源贡献的入门并不总是简单明了,而且可能很棘手。如果您是贡献的新手,请观看这些视频,您将掌握开始贡献开源项目所需的工具、知识和理解力。Better Specs 尤其需要您帮助完成以下任务。
→ 为新语言添加测试指导线(打开问题)。
→ 修复过时的最佳实践,这些最佳实践已随着时间的推移而改变(打开问题)。
→ 添加或更新现有翻译(启动问题)。
感谢您抽出宝贵时间,享受编码乐趣,从今天开始为您使用和喜爱的项目做出贡献。