自动化测试

    录影

    以下的现场课程录影,讲述了投影片 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

    1. before do
    2. @order = Order.new
    3. end
    4. context "when initialized" do
    5. it "should have default status is New" do
    6. expect(@order.status).to eq("New")
    7. end
    8. it "should have default amount is 0" do
    9. expect(@order.amount).to eq(0)
    10. end
    11. end

    RSpec程式码比起来更容易阅读,也更像是一种规格Spec文件,且让我们继续介绍下去。

    RSpec是一套Ruby的测试DSL(Domain-specific language)框架,它的程式比Test::Unit更好读,写的人更容易描述测试目的,可以说是一种可执行的规格文件。也非常多的Ruby on Rails专案采用RSpec作为测试框架。它又称为一种BDD(Behavior-driven development)测试框架,相较于TDDtest思维,测试程式的结果。BDD强调的是用spec思维,描述程式应该有什么行为。

    Gemfile中加入:

    1. group :test, :development do
    2. gem "rspec-rails"
    3. end

    安装:

    1. rails generate rspec:install

    以下指令会执行所有放在spec目录下的测试程式:

    1. bin/rake spec

    如果要测试单一档案,可以这样:

    语法介绍

    在示范怎么在Rails中写单元测试前,让我们先介绍一些基本的RSpec用法:

    describe和context

    describecontext帮助你组织分类,都是可以任意套叠的。它的参数可以是一个类别,或是一个字串描述:

    1. describe Order do
    2. describe "#amount" do
    3. context "when user is vip" do
    4. # ...
    5. end
    6. context "when user is not vip" do
    7. # ...
    8. end
    9. end
    10. end

    通常最外层是我们想要测试的类别,然后下一层是哪一个方法,然后是不同的情境。

    每个it就是一小段测试,在里面我们会用expect(…).to来设定期望,例如:

    1. describe Order do
    2. describe "#amount" do
    3. context "when user is vip" do
    4. user = User.new( :is_vip => true )
    5. order = Order.new( :user => user, :total => 2000 )
    6. expect(order.amount).to eq(1900)
    7. end
    8. it "should discount ten percent if total >= 10000" { ... }
    9. end
    10. context "when user is vip" { ... }
    11. end
    12. end

    除了expect(…).to,也有相反地expect(…).not_to可以用。

    before和after

    如同xUnit框架的setupteardown

    • before(:each) 每段it之前执行,默认写 before 就是 before(:each)
    • before(:all) 整段describe前只执行一次
    • after(:each) 每段it之后执行
    • after(:all) 整段describe后只执行一次

    范例如下:

    1. describe Order do
    2. describe "#amount" do
    3. context "when user is vip" do
    4. before(:each) do
    5. @user = User.new( :is_vip => true )
    6. end
    7. it "should discount five percent if total >= 1000" do
    8. @order.total = 2000
    9. expect(@order.amount).to eq(1900)
    10. end
    11. it "should discount ten percent if total >= 10000" do
    12. expect(@order.amount).to eq(9000)
    13. end
    14. end
    15. context "when user is vip" { ... }
    16. end
    17. end

    let 和 let!

    let可以用来简化上述的before用法,并且支援lazy evaluationmemoized,也就是有需要才初始,并且不同单元测试之间,只会初始化一次,可以增加测试执行效率:

    1. describe Order do
    2. describe "#amount" do
    3. context "when user is vip" do
    4. let(:user) { User.new( :is_vip => true ) }
    5. let(:order) { Order.new( :user => @user ) }
    6. end
    7. end
    8. end

    透过let用法,可以比before更清楚看到谁是测试的主角,也不需要本来的@了。

    let!则会在测试一开始就先初始一次,而不是lazy evaluation

    你可以先列出来预计要写的测试,或是暂时不要跑的测试,以下都会被归类成pending

    specify 和 example

    specifyexample都是it方法的同义字。

    Matcher

    1. expect { ... }.to raise_error
    2. expect { ... }.to raise_error(ErrorClass)
    3. expect { ... }.to raise_error("message")
    4. expect { ... }.to raise_error(ErrorClass, "message")

    不过别担心,一开始先学会用eq就很够用了,其他的Matchers可以之后边看边学,学一招是一招。再进阶一点你可以自己写MatcherRSpec有提供扩充的DSL

    Rails中的测试

    Rails中,RSpec分成数种不同测试,分别是Model测试、Controller测试、View测试、Helper测试、RouteRequest测试。

    Gemfile中加上

    1. gem 'rspec-rails', :group => [:development, :test]

    执行以下指令:

    1. $ bundle
    2. $ rails g rspec:install

    装了rspec-rails之后,rails g model 或 controller 时就会顺道建立对应的Spec档案了。

    如何处理Fixture

    Rails内建有Fixture功能可以建立假资料,方法是为每个Model使用一份YAML资料。Fixture的缺点是它是直接插入资料进数据库而不使用ActiveRecord,对于复杂的Model资料建构或关连,会比较麻烦。因此推荐使用这套工具,相较于Fixture的缺点是建构速度较慢,因此撰写时最好能注意不要浪费时间在产生没有用到的假资料。甚至有些资料其实不需要存到数据库就可以进行单元测试了。

    关于测试资料最重要的一点是,记得确认每个测试案例之间的测试资料需要清除,Rails默认是用关联式数据库的Transaction功能,所以每次之间增修的资料都会清除。但是如果你的数据库不支援(例如MySQLMyISAM格式就不支援)或是用如MongoDBNoSQL,那么就要自己处理,推荐可以试试Database Cleaner这套工具。

    Capybara简介

    RSpec除了可以拿来写单元程式,我们也可以把测试的层级拉高做整合性测试,以Web应用程式来说,就是去自动化浏览器的操作,实际去向网站服务器请求,然后验证出来的HTML是正确的输出。

    就是一套可以搭配的工具,用来模拟浏览器行为。使用范例如下:

    1. describe "the signup process", :type => :request do
    2. it "signs me in" do
    3. within("#session") do
    4. fill_in 'Login', :with => 'user@example.com'
    5. fill_in 'Password', :with => 'password'
    6. end
    7. click_link 'Sign in'
    8. end

    默认的 Capybara 是不会执行网页上的 JavaScript 的,如果需要测试JavaScriptAjax接口,可以安装额外安装 JavaScript Driver,但是缺点是测试会更耗时间,

    Guard是一种Continuous Testing的工具。程式一修改完存盘,自动跑对应的测试。可以大大节省时间,立即回馈。

    提供了更多Rails的专属Matchers

    SimpleCov用来测试涵盖度,也就是告诉你哪些程式没有测试到。有些团队会追求100%涵盖率是很好,不过要记得Coverage只是手段,不是测试的目的。

    CI server

    CI(Continuous Integration)服务器的用处是每次有人Commit就会自动执行编译及测试(Ruby不用编译,所以主要的用处是跑测试),并回报结果,如果有人送交的程式搞砸了回归测试,马上就有回馈可以知道。推荐第三方的服务包括:

    更多线上资源