自动化测试
录影
以下的现场课程录影,讲述了投影片 RSpec & TDD Tutorial 和 RSpec Mocks 的内容。
前言
软件测试可以从不同层面去切入,其中最小的测试粒度叫做Unit Test单元测试,会对个别的类别和方法测试结果如预期。再大一点的粒度称作Integration Test整合测试,测试多个元件之间的互动正确。最大的粒度则是Acceptance Test验收测试,从用户观点来测试整个软件。
其中测试粒度小的单元测试,通常会由开发者自行负责测试,因为只有你自己清楚每个类别和方法的内部结构是怎么设计的。而粒度大的验收测试,则常由专门的测试工程师来负责,测试者不需要知道程式码内部是怎么实作的,只需知道什么是系统应该做的事即可。
本章的内容,就是关于我们如何撰写自动化的测试程式,也就是写程式去测试程式。很多人对于自动化测试的印象可能是:
- 布署前作一次手动测试就够了,不需要自动化
- 写测试很无聊
- 测试很难写
- 写测试不好玩
- 我们没有时间写测试
时程紧迫预算吃紧,哪来的时间做自动化测试呢?这个想法是相当短视和业馀的想法,写测试有以下好处:
- 正确(Correctness):确认你写的程式的正确,结果如你所预期。一旦写好测试程式,很容易就可以检查程式有没有写对,大大减少自行除错的时间。
- 稳定(Stability):之后新加功能或改写重构时,不会影响搞烂之前写好的功能。这又叫作「回归测试」,你不需要手动再去测其他部分的测试,你可以用之前写好的测试程式。如果你的软件不是那种跑一次就丢掉的程式,而是需要长期维护的产品,那就一定有回归测试的需求。
- 设计(Design):可以采用TDD开发方式,先写测试再实作。这是写测试的最佳时机点,实作的目的就是为了通过测试。从使用API的呼叫者的角度去看待程式,可以更关注在接口而设计出更好用的API。
- 文件(Documentation):测试就是一种程式规格,程式的规格就是满足测试条件。这也是为什么RSpec称为Spec的原因。不知道API怎么呼叫使用时,可以透过读测试程式知道怎么使用。
其中光是第一个好处,就值得你学习如何写测试,来加速你的开发,怎么说呢?回想你平常是怎么确认你写的程式正确的呢? 是不是在命令列中实际执行看看,或是打开浏览器看看结果,每次修改,就重新手动重新整理看看。这些步骤其实可以透过用自动化测试取代,大大节省手工测试的时间。这其实是一种投资,如果是简单的程式,也许你手动执行一次就写对了,但是如果是复杂的程式,往往第一次不会写对,你会浪费很多时间在检查到底你写的程式的正确性,而写测试就可以大大的节省这些时间。更不用说你明天,下个礼拜或下个月需要再确认其他程式有没有副作用影响的时候,你有一组测试程式可以大大节省手动检查的时间。
那要怎么进行自动化测试呢?几乎每种语言都有一套叫做xUnit测试框架的测试工具,它的标准流程是 1. (Setup) 设定测试资料 2. (Exercise) 执行要测试的方法 3. (Verify) 检查结果是否正确 4. (Teardown) 清理还原资料,例如数据库,好让多个测试不会互相影响。
我们将使用RSpec来取代Rails默认的Test::Unit来做为我们测试的工具。RSpec是一套改良版的xUnit测试框架,非常风行于Rails社群。让我们先来简单比较看看它们的语法差异:
这是一个Test::Unit范例,其中一个test__开头的方法,就是一个单元测试,里面的_assert_equal方法会进行验证。个别的单元测试应该是独立不会互相影响的:
以下是用RSpec语法改写,其中的一个it区块,就是一个单元测试,里面的expect方法会进行验证。在RSpec里,我们又把一个小单元测试叫做example:
before do
@order = Order.new
end
context "when initialized" do
it "should have default status is New" do
expect(@order.status).to eq("New")
end
it "should have default amount is 0" do
expect(@order.amount).to eq(0)
end
end
RSpec程式码比起来更容易阅读,也更像是一种规格Spec文件,且让我们继续介绍下去。
是一套Ruby的测试DSL(Domain-specific language)框架,它的程式比Test::Unit更好读,写的人更容易描述测试目的,可以说是一种可执行的规格文件。也非常多的Ruby on Rails专案采用RSpec作为测试框架。它又称为一种BDD(Behavior-driven development)测试框架,相较于TDD用test思维,测试程式的结果。BDD强调的是用spec思维,描述程式应该有什么行为。
在Gemfile中加入:
group :test, :development do
gem "rspec-rails"
end
安装:
rails generate rspec:install
以下指令会执行所有放在spec目录下的测试程式:
bin/rake spec
如果要测试单一档案,可以这样:
语法介绍
在示范怎么在Rails中写单元测试前,让我们先介绍一些基本的RSpec用法:
describe和context
describe和context帮助你组织分类,都是可以任意套叠的。它的参数可以是一个类别,或是一个字串描述:
describe Order do
describe "#amount" do
context "when user is vip" do
# ...
end
context "when user is not vip" do
# ...
end
end
end
通常最外层是我们想要测试的类别,然后下一层是哪一个方法,然后是不同的情境。
每个it就是一小段测试,在里面我们会用expect(…).to来设定期望,例如:
describe Order do
describe "#amount" do
context "when user is vip" do
user = User.new( :is_vip => true )
order = Order.new( :user => user, :total => 2000 )
expect(order.amount).to eq(1900)
end
it "should discount ten percent if total >= 10000" { ... }
end
context "when user is vip" { ... }
end
end
除了expect(…).to,也有相反地expect(…).not_to可以用。
before和after
如同xUnit框架的setup和teardown:
before(:each)
每段it之前执行,默认写before
就是before(:each)
。before(:all)
整段describe前只执行一次after(:each)
每段it之后执行after(:all)
整段describe后只执行一次
范例如下:
describe Order do
describe "#amount" do
context "when user is vip" do
before(:each) do
@user = User.new( :is_vip => true )
end
it "should discount five percent if total >= 1000" do
@order.total = 2000
expect(@order.amount).to eq(1900)
end
it "should discount ten percent if total >= 10000" do
expect(@order.amount).to eq(9000)
end
end
context "when user is vip" { ... }
end
end
let 和 let!
let可以用来简化上述的before用法,并且支援lazy evaluation和memoized,也就是有需要才初始,并且不同单元测试之间,只会初始化一次,可以增加测试执行效率:
describe Order do
describe "#amount" do
context "when user is vip" do
let(:user) { User.new( :is_vip => true ) }
let(:order) { Order.new( :user => @user ) }
end
end
end
透过let用法,可以比before更清楚看到谁是测试的主角,也不需要本来的@
了。
let!则会在测试一开始就先初始一次,而不是lazy evaluation。
你可以先列出来预计要写的测试,或是暂时不要跑的测试,以下都会被归类成pending:
specify 和 example
specify和example都是it方法的同义字。
Matcher
expect { ... }.to raise_error
expect { ... }.to raise_error(ErrorClass)
expect { ... }.to raise_error("message")
expect { ... }.to raise_error(ErrorClass, "message")
不过别担心,一开始先学会用eq
就很够用了,其他的Matchers可以之后边看边学,学一招是一招。再进阶一点你可以自己写Matcher,RSpec有提供扩充的DSL。
Rails中的测试
在Rails中,RSpec分成数种不同测试,分别是Model测试、Controller测试、View测试、Helper测试、Route和Request测试。
在Gemfile中加上
gem 'rspec-rails', :group => [:development, :test]
执行以下指令:
$ bundle
$ rails g rspec:install
装了rspec-rails之后,rails g model 或 controller 时就会顺道建立对应的Spec档案了。
如何处理Fixture
Rails内建有Fixture功能可以建立假资料,方法是为每个Model使用一份YAML资料。Fixture的缺点是它是直接插入资料进数据库而不使用ActiveRecord,对于复杂的Model资料建构或关连,会比较麻烦。因此推荐使用FactoryGirl这套工具,相较于Fixture的缺点是建构速度较慢,因此撰写时最好能注意不要浪费时间在产生没有用到的假资料。甚至有些资料其实不需要存到数据库就可以进行单元测试了。
关于测试资料最重要的一点是,记得确认每个测试案例之间的测试资料需要清除,Rails默认是用关联式数据库的Transaction功能,所以每次之间增修的资料都会清除。但是如果你的数据库不支援(例如MySQL的MyISAM格式就不支援)或是用如MongoDB的NoSQL,那么就要自己处理,推荐可以试试这套工具。
Capybara简介
RSpec除了可以拿来写单元程式,我们也可以把测试的层级拉高做整合性测试,以Web应用程式来说,就是去自动化浏览器的操作,实际去向网站服务器请求,然后验证出来的HTML是正确的输出。
就是一套可以搭配的工具,用来模拟浏览器行为。使用范例如下:
describe "the signup process", :type => :request do
it "signs me in" do
within("#session") do
fill_in 'Login', :with => 'user@example.com'
fill_in 'Password', :with => 'password'
end
click_link 'Sign in'
end
默认的 Capybara 是不会执行网页上的 JavaScript 的,如果需要测试JavaScript和Ajax接口,可以安装额外安装 JavaScript Driver,但是缺点是测试会更耗时间,
Guard是一种Continuous Testing的工具。程式一修改完存盘,自动跑对应的测试。可以大大节省时间,立即回馈。
提供了更多Rails的专属Matchers
SimpleCov用来测试涵盖度,也就是告诉你哪些程式没有测试到。有些团队会追求100%涵盖率是很好,不过要记得Coverage只是手段,不是测试的目的。
CI server
CI(Continuous Integration)服务器的用处是每次有人Commit就会自动执行编译及测试(Ruby不用编译,所以主要的用处是跑测试),并回报结果,如果有人送交的程式搞砸了回归测试,马上就有回馈可以知道。推荐第三方的服务包括: