如何来描述(describe)你的method
首先要清楚你要描述的是什么类型的方法。用 Ruby 文档的一个惯例举例,提到类方法时使用.
(或者::
),提到实例方法的时候用#
来描述。
#BAD describe ‘the authenticate method for user‘ do describe ‘if the user is an admin‘ do
#GOOD describe ‘.authenticate‘ dodescribe ‘#admin?‘ do
使用上下文环境contexts
contexts 非常强大,它能让测试更清晰,有条理。在漫长的开发过程中,让你的测试一直保持高度的可读性。
#BAD it ‘has 200 status code if logged in‘ do response.should respond_with 200 end it ‘has 401 status code if not logged in‘ do response.should 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时用“when”或者"with"作为开头
保持简洁的description
一个测试的描述永远也不应该超过40个字符。如果超过了,那就应该用context来分割
#BAD it ‘has 422 status code if an unexpected params will be added‘ do
#GOOD context ‘when not valid‘ do it { should respond_with 422 } end
在这个例子中,我们用测试的期望 it { should respond_with 422 }
替换了描述中相关的 status code。 如果你用 rspec filename
执行这个测试,你依然能得到非常可读的测试报告。
#测试输出 when not valid it should respond with 422
唯一的测试条件
“唯一的测试条件”可以被宽泛的定义为“每一个测试都应该只有一个断言”。 这样做的能帮助你找到可能的问题,直接前往失败的测试,并且让你的代码具有可读。
在独立的单元测试中,你希望每一个具体的测试都只定义一个而且是只有一个行为。多重的测试期望条件在同一个具体测试中 说明你可能定义了多个行为。
不过,在像那些涉及了数据库,外部 Web Service,或者从一个系统到另一个系统的测试中,你可能会 消耗大量的资源来不断的重复一些准备工作,就因为要进行不同的单一条件测试。在这些非常消耗时间的测试中, 定义多个行为是可以接受的。
#GOOD (独立的) it { should respond_with_content_type(:json) } it { should assign_to(:resource) }
#GOOD(非独立的) it ‘creates a resource‘ do response.should respond_with_content_type(:json) response.should assign_to(:resource) end
测试所有可能性
测试是件好事,但如果你不进行边界测试,测试就不会真正的有效。有效和无效的边界情形都需要被测试,用下面的例子来做示范。
#destroy action before_filter :find_owned_resources before_filter :find_resource def destroy render ‘show‘ @consumption.destroy end
对于这个例子来说,非常常见的错误就是只测试这个资源有没有被摧毁。但这个 action 至少还包括了两个边界情形:当这个资源 找不到的时候或者这个资源没有权限被摧毁。记得这个通用的原则:考虑所有可能的输入并对他们都进行测试。
#bad it ‘shows the resource‘
#good 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
善用subject
如果你有好几个测试都是用了同一个 subject,使用subject{}
来避免重复。
#bad it { assigns(‘message‘).should match /it was born in belville/ } it { assigns(‘message‘).creator.should match /topolino/ }
#good subject { assigns(‘message‘) } it { should match /it was born in billville/ }
RSpec 也可以给 subject 命名。
#good subject(:hero) { hero.first } it "carries a sword" do hero.equipment.should include "sword" end
使用let和let!
当你需要给一个变量赋值时,使用 let
而不是 before
来创建这个实例变量。let
采用了 lazy load 的机制,只有在第一次用到的时候才会加载,然后就被缓存,直到测试结束。 这里有一个讲的非常好非且有深度的描述什么 是 let
的帖子Stackoverflow Answer。
#bad describe ‘#type_id‘ do before { @resource = FactoryGirl.create :device } before { @type = type.find @resource.type_id } it ‘sets the type_id field‘ do @resource.type_id.should equal(@type.id) end end
#good describe ‘#type_id‘ do let(:resource) { FactoryGirl.create :device } let(:type) { type.find resource.type_id } it ‘sets the type_id field‘ do resource.type_id.should equal(type.id) end end
#good 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!。这事一个在测试数据库查询或者scope语句中是非常有用的技巧
下面是let的实例
#good #这一段 let(:foo){Foo.new} #基本上完全等同这一段 def foo @foo ||= Foo.new end
不要用mock
只创建你需要的数据
如果你曾经在一个中型(小型也是)的项目工作过,你会发现跑测试针的是非常沉重的。解决这个问题就是不要载入你不需要的数据。
如果你需要大量的数据,可以使用:factory_girl
#good describe "user" describe ".top" do before { FactoryGirl.create_list(:user, 3) } it { User.top(2).should have(2).item } end end
选择Factory,抛弃fixtures
这个话题有点炒冷饭,但是还是值得被记住。不要使用 fixtures 是因为它们很难被控制和维护。 改用 factories 来减少冗长的数据准备过程。
#bad user = user.create( name: ‘genoveffa‘, surname: ‘piccolina‘, city: ‘billyville‘, birth: ‘17 agoust 1982‘, active: true )
#good user = FactoryGirl.create :user
还有一点很重要。当谈到单元测试的时候,最佳的实践是既不用 fixtures 也不用 factories。尽可能的把你的业务逻辑 放在那些不需要复杂的而又非常费时的 factories 或者 fixtures。这篇文章可以让你了解更多。
一目了然的matcher
使用易读的或是RSpec 自带的 matchers。
#bad lambda { model.save! }.should raise_error Mongoid::Errors::DocumentNotFound
#good expect { model.save! }.to raise_error Mongoid::Errors::DocumentNotFoundf
共用的测试
写测试非常棒,你会因此变得越来越自信。但到后来你会开始看到重复的代码出现在各个地方。用共用的测试来 DRY 你的测试。
#bad describe ‘get /devices‘ do let!(:resource) { FactoryGirl.create :device, created_from: user.id } let(:uri) { ‘/devices‘ } context ‘when shows all resources‘ do let!(:not_owned) { FactoryGirl.create factory } it ‘shows all owned resources‘ do page.driver.get uri page.status_code.should 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 page.status_code.should be(200) contains_resource resources.first page.should_not have_content resource.id.to_s end end end
#good describe ‘get /devices‘ do let!(:resource) { FactoryGirl.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
不过从我们的经验来看,共用的测试主要是用于 controller。因为 model 之间的行为迥异,他们(经常)没用太多共通的逻辑。
测你所见
尽可能详尽的测试你的 Model 和程序的行为(集成测试)。不要为 Controller 去写那些复杂而又没用的测试。
我一开始测试我的项目的时候,我测的就是 Controller,现在我却不这么做了。 现在我只用 RSpec 和 Capybara 来写一些集成测试。为什么?因为我真的觉得你应该 测试那些你能看到的,而 Controller 测试对于我来说是多余的。你最终会发现,你大部分的测试 都是涉及到 Model。集成测试能被轻松的整理到共用的测试来构建清晰而又可读的测试。
这是一个在 Ruby 社区中非常开放的争论,而且两边都是有理有据。支持测试 Controller 的那一方 会告诉你集成测试并不能覆盖所有的情况,而且很慢。
但他们都错了。你可以很轻易的覆盖所有的情形,并且使用一些像 Guard 这样的自动化测试工具来跑一个个单独的测试。 这样你就可以在不停止你的工作流程的情况下闪电般的只运行那些你正需要的测试。
别用should
当你在描述你的测试的时候,不要使用 should,使用第三人称现在时。更进一步,你可以使用新的expectation语法。
#bad it ‘should not change timings‘ do consumption.occur_at.should == valid.occur_atend
#good it ‘does not change timings‘ do expect(consumption.occur_at).to equal(valid.occur_at) end
用 the should_not 和the should_clean 这两个 Gem 在 RSpec 中贯彻这条实践并清除那些以 should 开头的测试。
用Guard进行自动化测试
每次你修改了你的项目就要重新跑所有的测试用力真的是一种负担。这会消耗很多时间并且打断了你的工作。 使用 Guard 你可以自动化的运行那些和你正在修改的测试,Model,Controller 或者文件有关的测试。
#good bundle exec guard
这是一个有些基本加载规则的 Guardfile 的例子。
#good guard ‘rspec‘, cli: ‘--drb --format fuubar --color‘, version: 2 do # 执行所有被修改的测试 watch(%r{^spec/.+_spec\.rb$}) # 执行与 lib 文件夹下有关联的文件被修改的测试 watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } # 执行与被修改 Model 相关的测试 watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } # 执行与被修改的 View 相关的测试 watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } # 执行与被修改的 Controller 相关的集成测试 watch(%r{^app/controllers/(.+)\.rb}) { |m| "spec/requests/#{m[1]}_spec.rb" } # 当 application_controller 被修改时执行所有的集成测试 watch(‘app/controllers/application_controller.rb‘) { "spec/requests" }end
Guard 虽然好用但是它还是无法满足你搜有的需求。有时候你的 TDD 工作流程需要一些能够测试你想要测试文件的快捷键来让它变得完美。 而在你需要 push 代码的时候用 rake task 来跑完整的测试。这里有些给 Vim 用的快捷键配置。
了解更多关于 Guard RSpec.
伪装HTTP请求
有时候你需要用到一些外部的服务。在你不能真的使用这些外部服务的时候你应该用类似 webmock 这样的工具来进行伪装。
#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 "gets a not authorized notification" do page.driver.get uri page.should have_content ‘Access denied‘ end end
了解更多关于 webmock 和VCR。 这还有一个非常不错的关于如何结合他们使用的视频