測試 Testing
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
關於寫測試,很多人的第一印象可能是:
- 寫測試很無聊
- 測試很難寫
- 寫測試不好玩
- 我們沒有時間寫測試
時程緊迫預算吃緊,哪來的時間做自動化測試呢?這個想法是短視的,寫測試其實有以下好處:
- 確認你寫的程式的正確,結果如你所預期。一旦寫好測試程式,很容易就可以檢查程式有沒有寫對。
- 之後新加功能或改寫重構時,不會影響搞爛其他程式。這又叫作「回歸測試」,你不需要手動再去測其他部分的測試,你可以用之前寫好的測試程式。
- 可以採用*TDD*開發方式,先寫測試再實作。這是寫測試的最佳時機點,實作的目的就是為了通過測試。從使用*API*的呼叫者的角度去看待程式,可以關注在介面而設計出更好用的*API*。
- 測試就是一種程式規格,程式的規格就是滿足測試條件。這也是為什麼*RSpec*稱為*Spec*的原因。不知道*API*怎麼呼叫使用時,可以透過讀測試程式知道怎麼使用。
其中光是第一個好處,就值得你學習如何寫測試,來加速你的開發,怎麼說呢?回想你平常是怎麼確認你寫的程式正確的呢? 是不是在命令列中實際執行看看,或是打開瀏覽器看看結果,每次修改,就重新手動重新整理看看。這些步驟其實可以透過用自動化測試取代,大大節省手工測試的時間。也其實是一種投資,如果是簡單的程式,也許你手動執行一次就寫對了,但是如果是複雜的程式,往往第一次不會寫對,你會浪費很多時間在檢查到底你寫的程式的正確性,而寫測試就可以大大的節省這些時間。更不用說你明天,下個禮拜或下個月需要再確認其他程式有沒有副作用影響的時候,你有一組測試程式可以大大節省手動檢查的時間。
在這一章,我們將使用RSpec來取代Rails預設的Test::Unit來做為我們測試的工具。RSpec是一套改良版的xUnit測試框架,讓我們先來比較看看:
幾乎每種語言都有一套基於xUnit測試框架的測試工具,讓你可以測試軟體中的基本元件,也就是類別和方法。簡單來說,每個單元測試的流程是:設定測試資料、然後執行要測試的方法、最後檢查結果是否正確。
這是一個Test::Unit範例:
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的語法則是寫成:
describe Order do
before do
@order = Order.new
end
context "when initialized" do
it "should have default status is New" do
@order.status.should == "New"
end
it "should have default amount is 0" do
@order.amount.should == 0
end
end
end
光看程式有沒有覺得非常容易了解跟閱讀,也更像是一種規格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", "~> 2.0"
gem "rspec-rails", "~> 2.0"
end
安裝:
rails generate rspec:install
如何執行測試:
bundle exec rake spec
rake spec
會先執行一次rake db:test:prepare
建立測試資料庫。
測試單一檔案,例如:
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和should
每個it就是一小段測試,在裡面我們會用should來設定期望,例如:
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 )
order.amount.should == 1900
end
it "should discount ten percent if total >= 10000" { ... }
end
context "when user is vip" { ... }
end
end
除了should,也有相反地should_not可以用。
before和after
如同xUnit框架的setup和teardown:
before(:each)
每段it之前執行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
@order.amount.should == 1900
end
it "should discount ten percent if total >= 10000" do
@order.total = 10000
@order.amount.should == 9000
end
end
context "when user is vip" { ... }
end
end
pending
可以先列出來打算要寫的測試:
describe Order do
describe "#paid?" do
it "should be false if status is new"
it "should be true if status is paid or shipping" do
pending
end
end
end
Matcher
上述的should後面可以接各種Matcher,例如:
target.should be_true
# targer.should == true
target.should be_false
# targer.should == false
target.should be_nil
# targer.should == nil
可以檢查型別、方法:
target.should be_a_kind_of(Array)
# target.class.should == Array
target.should be_an_instance_of(Array)
# target.class.should == Array
target.should respond_to(:foo)
# target.repond_to?(:foo).should == true
可以檢查 Array、Hash:
target.should have_key(:foo)
# target[:foo].should_not == nil
target.should include(4)
# target.include?(4).should == true
target.should have(3).items
# target.items.length == 3
任何 be_ 開頭都可以:
target.should be_empty
# target.empty?.should == true
target.should be_blank
# target.blank?.should == true
target.should be_admin
# target.admin?.should == true
不過別擔心,一開始先學會用should ==
就很夠用了,其他的Matchers可以之後邊看邊學,學一招是一招。再進階一點你可以自己寫Matcher,RSpec有提供擴充的DSL。
Expect to
期望一段程式會改變某個值或丟出例外。例如,改變值:
describe Order do
describe "#ship!" do
context "with paid" do
it "should update status to shipping" do
expect {
order.ship!
}.to change { order.status }.from("new").to("shipping")
end
end
context "without paid" { ... }
end
end
丟出例外:
describe Order do
describe "#ship!" do
context "with paid" do
it "should raise NotPaidError" do
expect {
order.paid? = false
order.ship!
}.to raise_error(NotPaidError)
end
end
context "with paid" { ... }
end
end
RSpec Mocks
用假的物件替換真正的物件,作為測試之用。主要用途有:
- 無法控制回傳值的外部系統 (例如第三方的網路服務)
- 建構正確的回傳值很麻煩 (例如得準備很多假資料)
- 可能很慢,拖慢測試速度 (例如耗時的運算)
- 有難以預測的回傳值 (例如亂數方法)
- 還沒開始實作 (特別是採用*TDD*流程)
如何使用Mocks超出本書範圍。
Rails中的測試
在Rails中,RSpec分成數種不同測試,分別是Model測試、Controller測試、View測試、Helper測試、Route測試。
安裝
編輯Gemfile:
group :test, :development do
gem "rspec"
gem "rspec-rails"
end
輸入bundle
安裝,接著輸入:
rails g rspec:install
這樣就會建立出spec目錄來放測試程式,本來的test目錄就不用了。
Model 測試
裝了rspec-rails之後,rails g model時也會順道建立對應的Spec檔案。這裡我們來寫點Event model的測試吧,延續RESTful 與表單設計中操作 Resources 狀態一節所示範的方法為例,新增spec/models/event_spec.rb如下:
require 'spec_helper'
describe Event do
before do
@event = Event.new( :name => "foobar" )
end
describe ".closed?" do
it "should return true if status is CLOSED" do
@event.status = "CLOSED"
@event.closed?.should be_true
end
it "should return false if status is not CLOSED" do
@event.status = "OPEN"
@event.closed?.should be_false
end
end
describe ".open?" do
it "should return true if status is OPEN" do
@event.status = "OPEN"
@event.open?.should be_true
end
it "should return false if status is not OPEN" do
@event.status = "CLOSED"
@event.open?.should be_false
end
end
describe "open!" do
it "should set status to OPEN" do
@event.open!
@event.status.should == "OPEN"
end
end
describe "close!" do
it "should set status to CLOSED" do
@event.close!
@event.status.should == "CLOSED"
end
end
end
要怎麼執行測試呢?輸入bundle exec rake spec
就會根據目前的開發資料庫Schema建一個測試用資料庫,然後執行所有spec目錄下的_spec.rb
檔案結尾的測試。
如果測試資料庫已經建好了,例如執行過rake spec
或是bundle exec rake db:test:prepare
之後,你也可以單獨執行測試bundle exec rspec spec/models/event_spec.rb
。