自动化测试
自动化测试
Developer testing isn’t primarily about verifying code. It’s about making great code. If you can’t test something, it might be your testing skills failing you but it’s probably your code code’s design. Testable code is almost always better code. - Chad Fowler
课程投影片
录影
以下的现场课程录影,讲述了投影片 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 方法会进行验证。个别的单元测试应该是独立不会互相影响的:
class OrderTest < Test::Unit::TestCase
def setup
@order = Order.new
end
def test_order_status_when_initialized
assert_equal @order.status, "New"
end
def test_order_amount_when_initialized
assert_equal @order.amount, 0
end
end
以下是用 RSpec 语法改写,其中的一个 it 区块,就是一个单元测试,里面的 expect 方法会进行验证。在 RSpec 里,我们又把一个小单元测试叫做 example :
describe Order do
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
end
RSpec 程式码比起来更容易阅读,也更像是一种规格 Spec 文件,且让我们继续介绍下去。
RSpec简介
RSpec 是一套 Ruby 的测试 DSL(Domain-specific language) 框架,它的程式比 Test::Unit 更好读,写的人更容易描述测试目的,可以说是一种可执行的规格文件。也非常多的 Ruby on Rails 专案采用 RSpec 作为测试框架。它又称为一种 BDD(Behavior-driven development) 测试框架,相较于 TDD 用 test 思维,测试程式的结果。 BDD 强调的是用 spec 思维,描述程式应该有什么行为。
安装RSpec与RSpec-Rails
在 Gemfile 中加入:
group :test, :development do
gem "rspec-rails"
end
安装:
rails generate rspec:install
这样就会建立出 spec 目录来放测试程式,本来的 test 目录就用不着了。
以下指令会执行所有放在 spec 目录下的测试程式:
bin/rake spec
如果要测试单一档案,可以这样:
bundle exec rspec spec/models/user_spec.rb
语法介绍
在示范怎么在 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
每个 it 就是一小段测试,在里面我们会用 expect(…).to 来设定期望,例如:
describe Order do
describe "#amount" do
context "when user is vip" do
it "should discount five percent if total >= 1000" 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 )
@order = Order.new( :user => @user )
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
@order.total = 10000
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
你可以先列出来预计要写的测试,或是暂时不要跑的测试,以下都会被归类成 pending :
describe Order do
describe "#paid?" do
it "should be false if status is new"
xit "should be true if status is paid or shipping" do
# this test will not be executed
end
end
end
specify 和 example
specify 和 example 都是 it 方法的同义字。
Matcher
上述的 expect(…).to 后面可以接各种 Matcher ,除了已经介绍过的 eq 之外,在 <https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers> 官方文件上可以看到更多用法。例如验证会丢出例外:
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 测试。
安装 Rspec-Rails
在 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 ,那么就要自己处理,推荐可以试试 Database Cleaner 这套工具。
Capybara简介
RSpec 除了可以拿来写单元程式,我们也可以把测试的层级拉高做整合性测试,以 Web 应用程式来说,就是去自动化浏览器的操作,实际去向网站服务器请求,然后验证出来的 HTML 是正确的输出。
Capybara 就是一套可以搭配的工具,用来模拟浏览器行为。使用范例如下:
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
end
默认的 Capybara 是不会执行网页上的 JavaScript 的,如果需要测试 JavaScript 和 Ajax 接口,可以安装额外安装 JavaScript Driver,但是缺点是测试会更耗时间,
其他可以搭配测试工具
Guard 是一种 Continuous Testing 的工具。程式一修改完存盘,自动跑对应的测试。可以大大节省时间,立即回馈。
Shoulda 提供了更多 Rails 的专属 Matchers
SimpleCov 用来测试涵盖度,也就是告诉你哪些程式没有测试到。有些团队会追求 100% 涵盖率是很好,不过要记得 Coverage 只是手段,不是测试的目的。
CI server
CI(Continuous Integration) 服务器的用处是每次有人 Commit 就会自动执行编译及测试(Ruby 不用编译,所以主要的用处是跑测试),并回报结果,如果有人送交的程式搞砸了回归测试,马上就有回馈可以知道。推荐第三方的服务包括:
- <https://travis-ci.org>
- <https://www.codeship.io>
- <https://circleci.com>
如果自己架设的话,推荐老牌的Jenkins。
