加速測試的方法
這里所說的“速度”有兩層含義。
其一,當然是測試運行所用的時間。我們這個小程序的測試已經開始出現慢的趨勢假設測試會隨著程序一起增長,那么測試速度就會越來越慢。我們的目標是保持代碼的可維護行,但又不破壞RSpec為我們提供的代碼可讀性。
其二,開發人員怎樣快速編寫清晰明了的測試。
- 第一類,rspec的使用技巧
- 使用let
- 使用馭件(mock)和使用樁件(stub)
- 把慢的測試單獨提出來
- 分類Gem包
- 第二類,加載rails的環境
- database_cleaner
- spring
- zeus
- spork
- 第三類,不加載rails環境
- 修改傳統測試
使用let
到目前為止,為測試準備通用數據時,我們使用的方法是在before :each塊中定義實例變量。還有一種RSpec用戶更樂意選擇的是使用let()。
let()有兩個好處:
- 不用賦值給實例變量就可以緩存值。
- 定義的變量是“惰性計算的”,不調用就不會執行賦值操作。
使用馭件和使用樁件
馭件(mock)是用來替代真實對象的測試對象,也被稱為“測試替身”(test double)。 馭件有點類似通過Factory Girl生成的對象,但不會改動數據庫中的數據。所以速度快一些。
樁件(stub)是對指定對象方法的重寫,返回一個預設的值。也就是說,樁件雖是個虛假方法,但調用時會返回一個真實的值供測試使用。樁件經常用來重寫方法的默認功能,特別是在頻繁操作數據庫或網絡密集型交互中。
例如:
要創建聯系人的馭件,可以使用Factory Girl提供的build_stubbed()方法。
這個方法會生成一個假冒對象,可以響應很多方法,例如firstname lastname 和fullname。不過所生成的對象不會存入數據庫。
要為Contact模型的方法創建樁件,可以使用下面這樣的代碼,
allow(Contact).to receive(:order).with('lastname,firstname').and_return([contact])
這里我們重現了Contact模型的order作用域,傳入一個字符串,指定SQL查詢結果的排序方式(按照姓和名字排序) 然后指明希望得到的結果,只有一個元素的數組,元素contact可能是在前面創建的。
傳統的慢的測試
describe "GET #show" do
let(:widget) { create(:widget) }
it "assigns the required 1 to @1" do
get :show, id: widget
expect(assigns(:widget)).to eq widget
end
it "renders the :show template " do
get :show, id: widget
expect(response).to render_template :show
end
end
bundle exec rspec spec/controllers/widgets_controller_spec.rb --line_number 4
Finished in 0.29162 seconds
改進之后的快的測試
describe "GET #show more faster" do
let(:widget){ build_stubbed(:widget, name: 'zhangsan', email: 'zs@126.com') }
before :each do
Widget.stub(:persisted?).and_return(true)
Widget.stub(:order).with('name, email').and_return([widget])
Widget.stub(:find).with(widget.id.to_s).and_return(widget)
Widget.stub(:save).and_return(true)
end
before :each do
Widget.stub(:find).with(widget.id.to_s).and_return(widget)
get :show, id: widget
end
it "assigns the requested widget to @widget" do
expect(assigns(:widget)).to eq widget
end
it "renders the :show template" do
expect(response).to render_template :show
end
end
bundle exec rspec spec/controllers/widgets_controller_spec.rb --line_number 18
Finished in 0.06785 seconds
速度提升 76%
分析: 使用let()把一個馭件賦值給widget。然后為Widget 模型和widget實例創建了一些樁件。在控制器中,我們希望能在Widget類和widget實例上調用一些ActiveRecord提供的方法。所以為這些方法創建了樁件,返回的結果和實際的ActiveRecord方法一樣。本例中全部的測試數據都由馭件和樁件提供,沒有操作數據庫,也沒有調用Widget模型。
這段測試的優點是,比之前的測試更獨立了,現在只需要關注控制器的動作,不用擔心模型或數據庫等,這么做當然也有缺點,獨立是付出了代價的,這段測試的代碼量增加了不少。
把慢的測試單獨提出來
首先,運行全部的測試 使用 bundle exec rspec spec/ -p
得出最慢的幾個測試
然后,把慢的測試都都打上 slow: true 的標簽,如下
describe WidgetsController, slow: true do
describe "GET #show" do
#慢的測試
end
end
最后, 在spec/spec_helper.rb文件中
RSpec.configure do |config|
#config.filter_run focus: true
config.filter_run_excluding slow: true
end
執行,
運行快的測試
bundle exec rspec spec -p
運行慢的測試
bundle exec rspec spec --tag slow -p
分類Gem包
把production、 develop、 test三種環境的Gem包分類加載到
各自的group下,該功能可以提高2%-3%。主要rails啟動的時候不需要加載額外的Gem包、從而大幅度提高rails的啟動速度。
充分正確使用database_cleaner
為什么要使用database_cleaner?
Rsepc進行測試的時候,如果有一個用例需要創建并保存到數據庫中,當再一次進行測試的時候,就會提示該對象已經存在了,創建失敗了。所以想要保證每次測試都能正常執行,需要在每次測試用例執行完畢之后將數據庫清空。
如:
it { expect {Deal.make!(create_time: Time.now)}.to
change{ Deal.count }.from(0).to(1) }
這個測試在運行的時候就會經常出錯,所以要使用database_cleaner來清空測試數據庫。
database_cleaner 的三種策略
Deletion
This means the database tables are cleaned using a
delete + recreate strategy. In SQL this means using
the DROP TABLE + CREATE TABLE statements. This strategy
would be considered the slowest, since you have to not
only delete the table data, but also the whole
table structure and then recreate it back.
However in case of problems with other methods
this can be considered the safest fallback method.
Drop Table + Create Table ,不僅要刪除數據,還要刪除表結構,最后還得重新創建表,不過這是最徹底最安全的方式。
Truncation
This means the database tables are cleaned using the
SQL TRUNCATE TABLE command. This will simply empty the table
immidiately, without deleting the table structure itself.
Truncate Table 不刪除表結構。
Transaction
This means using BEGIN TRANSACTION statements coupled
with ROLLBACK to roll back a sequence of previous
database operations. Think of it as an "undo button"
for databases. I would think this is the most frequently
used cleaning method, and probably the fastest since
changes need not be directly committed to the DB.
Begin Trasaction + Rollback
那么transaction > truncation , deletion
http://stackoverflow.com/questions/11419536/postgresql-truncation-speed/11423886#11423886
我們常用的配置在 spec/spec_helper.rb
Rspec.configure do |config|
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
spring
安裝:
- 在Gemfile中安裝spring
gem "spring", group: :development
gem "spring-commands-rspec" - 然后執行
bundle install
spring啟動圖

常用命令:
spring 用來啟動spring
spring status 用來表示啟動的狀態
time spring rspec spec/ 使用spring 來運行測試
測試結果對比
用spring 來測試rspec_test 所需要的時間
Finished in 3.46 seconds
100 examples, 0 failures, 18 pending
Randomized with seed 8738
real 0m5.171s
user 0m0.060s
sys 0m0.016s
不使用spring測試rspec_test所需要的時間
Finished in 3.4 seconds
100 examples, 0 failures, 18 pending
Randomized with seed 285
real 0m5.362s
user 0m2.804s
sys 0m0.212s
使用spring測試速度提升3%
zeus
安裝
gem install zeus
對于rspec來說,去除spec/spec_helper.rb中的文件
require 'rspec/autotest'
require 'rspec/autorun'
由于spec/spec_helper.rb文件會自動加上上面配置,所以會導致出現重復測試的情況。
zeus啟動圖

常用命令
zeus console
相等于 rails c
zeus server
相等于 rails s
zeus test spec/
相等于 rspec spec/
zeus generate model omg
相等于rails g model omg
測試結果對比
使用zeus測試,結果為
time zeus test spec/
Finished in 3.26 seconds
99 examples, 0 failures, 18 pending
Randomized with seed 0
real 0m3.600s
user 0m0.048s
sys 0m0.020s
不使用zeus測試,結果為
Finished in 3.4 seconds
100 examples, 0 failures, 18 pending
Randomized with seed 285
real 0m5.362s
user 0m2.804s
sys 0m0.212s
使用zeus 測試速度提升32%
spork
安裝
-
在Gemfile中添加
gem 'spork', '~> 1.0rc'
安裝gem
bundle install
執行
spork rspec --bootstrap
該命令會在spec_helper中添加自己的模板代碼
Spork.prefork代碼塊中的東西,在Spork啟動的時候就加載
Spork.each_run來表示每次運行rspec都會加載
spork 啟動圖

常用命令
spork
用來啟動spork
time bundle exec rspec spec/ --drb
用來測試
測試結果對比
用來測試rspec_test
使用spork
Finished in 3.65 seconds
100 examples, 0 failures, 18 pending
Randomized with seed 46006
real 0m4.437s
user 0m0.628s
sys 0m0.072s
不使用spork的結果
Finished in 3.4 seconds
100 examples, 0 failures, 18 pending
Randomized with seed 285
real 0m5.362s
user 0m2.804s
sys 0m0.212s
使用spork測試速度提升17%
使用不加載rails的環境,進行測試的
app/controller/TracksController
class TracksController < ApplicationController
def index
signed_in_user
end
def new
@track = Track.new
end
def create
feed = params[:track]["feed"]
@track = TrackParserService.parse(feed)
unless @track.valid?
render :action => 'new'
return
end
@track.save_with_user!(signed_in_user)
render :action => 'index'
end
private
def signed_in_user
# No authentication yet
@user ||= User.first
end
end
/spec/units/controller/tracks_controller_spec.rb測試這樣寫
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", ".."))
$: << File.join(APP_ROOT, "app/controllers")
# A test double for ActionController::Base
module ActionController
class Base
def self.protect_from_forgery(xx); end
end
end
class User; end
class Track; end
class TrackParserService; end
require 'application_controller'
require 'tracks_controller'
describe TracksController do
let(:controller) { TracksController.new }
specify "index action returns the signed in user" do
# setup
user = stub
User.stub(:first).and_return user
# execute action under test
returned_user = controller.index
# verify
returned_user.should == user
controller.instance_variable_get(:@user).should == user
end
specify "new action returns an instance of Track" do
# setup
track = stub
Track.stub(:new).and_return track
# execute action under test
new_track = controller.new
# verify
new_track.should == track
controller.instance_variable_get(:@track).should == track
end
context "when the model is not valid" do
it "renders action => 'new'" do
# define a method for params - TracksController is not aware of it
controller.class.send(:define_method, :params) do
{:track => "feed"}
end
track = stub(:valid? => false)
TrackParserService.stub(:parse).and_return(track)
render_hash = {}
# hang on to the input hash the render method is invoked with
# I'll use it to very that the render argument is correct
controller.class.send(:define_method, :render) do |hash_argument|
render_hash = hash_argument
end
controller.create
# verify the render was called with the right hash
render_hash.should == { :action => 'new' }
end
end
end
測試所需要時間 Finished in 0.00202 seconds
僅需要2毫秒
瀑布開發模式

優點:
- 為項目提供了按階段劃分的檢查點。
- 當前一階段完成后,您只需要去關注后續階段。
缺點:
- 在項目各個階段之間極少有反饋。
- 只有在項目生命周期的后期才能看到結果。
- 通過過多的強制完成日期和里程碑來跟蹤各個項目階段。
- 開發階段出現的bug往往有些在后期測試中查不到,并且可能出現查到之后,修改會非常困難。
[關鍵詞,開發階段bug,代碼越寫越不放心,測試代碼覆蓋率,rabl,后期才能看到結果]
TDD介紹
也就是 Test Driven Development--測試驅動開發,其實是一種開發方式的巨大提高。它
提出了一種新的開發方式:以測試為驅動。在此,我仍然想引用一個曾經看過的ThoughtWorks的一個人的Blog中的一句話:“什么是TDD?TDD就是把你的需求用測試給描述出來。”
通俗易懂的概述: TDD就是在寫每個方法的時候建一個測試方法,等方法寫完之后,運行測試方法來檢查運行結果。

歸根到底,TDD的實質仍然是以需求來驅動開發,只是,TDD中把需求進一步寫成了測試,那
成了測試驅動開發了。
[關鍵詞,驅動]
這么做的好處是什么?我想至少有以下這么幾條:
1、你的代碼是可測試的。[關鍵詞,可測試]
2、你的代碼完全反應了需求。[關鍵詞,用測試描述需求,測試驅動代碼,代碼反應需求]
3、通過測試驅動,會規范你的代碼和結構,甚至架構。[關鍵詞,測試影響架構]
通常對于TDD的誤區:
誤區一:沒什么用處,多此一舉
有人會說,我在編寫方法的時候本來就是考慮了這些因素的,并且我在調用的時候也加了判斷條件。我以前從來不寫單元測試,系統“一直按我期望的那樣正常運行”。
問題是,你相信你寫的代碼嗎,你敢保證每一個方法,你都這樣去思考過嗎:它應該返回某個期望的值,如果參數是一些邊界值,它應該返回這樣而不是讓系統崩潰。也許大多數時候,你都是匆匆想一下,馬上就寫方法名,方法體,你也許考慮了主要的因素,但是是否這個方法能處理你沒有預期到的條件。[關鍵詞,在測試過程中充分考慮臨界值 ]
你是否有這樣的感覺,越是軟件做到后面,你越不敢保證軟件不會出Bug;當別人在一邊豎起大拇指稱贊系統多么穩定的時候,你的心總是懸空的,你知道它隨時都有可能出現問題。[關鍵詞,TDD增強開發信心]
TDD要求在每個方法定義編寫前,去考慮方法的各種可能情況,并且直到測試通過,才開始編寫下一個方法。它是你在編寫最小單元功能的時候,確保每一個功能單元是更加健壯的,因此稱作單元測試。
TDD的神奇力量不在于那段測試代碼,那只不過是一個普通方法的調用,驗證而已。
TDD最寶貴的是:促使你在設計每個最小功能的時候,花一點時間去仔細思考這個最小單元(方法)的各種邊界條件,確保每一個單元更加健壯,穩定。這樣,到最后,你的整個系統也更加可靠穩當。
只有經過測試的代碼才是可靠的。雖然Bug不可避免,但是,如果你做了嚴格的單元測試,你會對你的代碼有更多的信心。
誤區二:浪費時間
TDD要求在每個方法定義編寫之前,先寫測試代碼,即你要花一點時間去思考這個方法的各種邊界條件,調用時會出現的各種情況。
這對于我們平時總是拿到一個功能,就開始定義類寫方法相比較,卻是是會花點時間。但是如果最終比較,它并不浪費時間。
你是否有這樣的感覺,到一個比較大的功能快完成的時候,你會花很多時間去調試。到后面,每一個Bug的調試,都會花費相當大的時間去定位和排錯。常常,我們在一大堆斷點之間跳來跳去,只是因為某個引用為nil。并且斷點調試并不是那么順利的,有時候你需要運行幾次才能夠定位到bug的地方。幸運的是,也許你憑經驗知道大概的位置,這可以范圍,但是不可避免的是,你需要花費更多的時間。
而經過單元測試,每一個方法都經過了足夠仔細的考慮,這將大大減少后期Bug的頻率。原因很簡單,你在設計一小塊功能的時候,也許考慮得比較仔細,但是當一個單元被整合進一個大的系統,在復雜的系統環境下,你沒有考慮到的因素就暴露出來了。并且系統越到后面,問題越多。[關鍵詞,不積跬步無以至千里不積小流無以成江海]
誤區三: 100%的代碼覆蓋率
選擇覆蓋率的標準時,應該考慮所用的技術、語言及開發工具等,通常會因為某些功能沒有測到,而是因為語言的設計和API的設計風格使得100%的覆蓋率不太現實而已。
一大早,一個年輕的程序員問大師:
“我準備寫一些單元測試用例。代碼覆蓋率應該達到多少為好?”
大師回答道:
“不要考慮代碼覆蓋率,只要寫出一些好的測試用例即可。”
年輕的程序員很高興,鞠躬,離去。
之后沒多久,第二個程序員問了大師同樣的問題。
大師指著一鍋燒沸的水說:
“我應該往這個鍋里放多少米?”
這個程序員看起來被難住了,回答道:
“我怎么會有答案?這取決于要給多少人吃,他們餓不餓,有什么菜,你有多少米,等等。”
“完全正確,”大師說。
第二個程序員很高興,鞠躬,離去。
末了,來了第三個程序員問了大師同樣的關于代碼覆蓋率的問題。
“百分之八十,不能少!”大師一拳錘在桌子上,用嚴厲的口氣回答道。
第三個程序員很高興,鞠躬,離去。
回復完這個之后,一個年輕的實習生走到大師身邊:
“大師,今天我無意中聽到了你對同一個代碼覆蓋率問題給出了三個不同的答案。為什么?”
大師從椅子上站起來:
“給我泡點新茶,我們聊聊這個?!?br>
當杯子里倒滿了冒著熱氣的綠茶后,大師開始說:
“這第一個程序員是個新手,剛剛開始學測試。目前他有大量的程序都沒有測試用例。他有很長的路要走;現在對他要求代碼覆蓋率只會打擊他,沒有什么用處。最好是讓他慢慢的學會寫一些測試用例,測試一下。他可以以后再考慮代碼覆蓋率。”
“而這第二個程序員,不論對編程還是測試都是十分的有經驗。我以問作答,問她應該往鍋里放多少米,使她明白決定測試用例多少的因素有很多,她比我更知道這些因素——畢竟是她自己的代碼。對這個問題沒有一個簡單的、直接的答案。以她的聰明完全能明白這個道理,正確的完成任務?!?br>
“我明白了,”年輕的實習生說,“但是如果沒有一個簡單直接的答案,那你為什么告訴第三個程序員‘百分之八十,不能少’呢?”
大師笑的前仰后合,綠茶都噴了出來。
“這第三個程序員只想得到一個簡單的答案——即使根本沒有簡單的答案 … 而且即使有答案她也不會按答案做。”
[關鍵詞,代碼覆蓋率,在寫好測試用例之上,當然是越多越好]
TDD的好處
好處一:促進代碼規范,設計結構合理,更遵循好的設計原則
剛開始接觸單元測試是會遇到挫折的,因為你會發現你編寫的方法難以測試。比如參數太依賴另一個方法或者對象,參數不可構造,方法太復雜,功能混亂導致邊界條件太多,等等,這些都是不良的設計。
遵循好的設計原則,比如單一職責,方法有清晰單一的任務,比如依賴于接口而不是實現的參數,不僅有助于減小耦合,在測試的時候更容易構造接口實現的參數等等。因此單元測試反過來促進你遵循更好的設計思想。
好處二:精準的定位錯誤的地方
因為測試的是最小的功能單元,能最小時間代價的獲取錯誤位置和原因。
好處三:減少調試時間
前面我們分析了,在系統后期調試會花費的時間代價
好處四:更健壯,可靠的代碼,可以睡好覺
發現,開始TDD之后,我對代碼更加有信心,不會時時擔心這會出問題你也會出問題,雖然Bug難免,但是經過測試的代碼更加可靠,這樣是不是能多睡覺,少加班呢,更重要的是減少不少焦慮細胞。
TDD的三條原則
1、You are not allowed to write any production code unless it is to make a failing unit test pass.
除非為了使一個失敗的unit test通過,否則不允許編寫任何產品代碼[關鍵詞,先寫測試]
2、You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
在一個單元測試中只允許編寫剛好能夠導致失敗的內容
在unit test中,你不能編寫太多的內容,只要一出現該unit test代碼不能編譯通過,或者斷言失敗,就必須停下來開始編寫產品代碼。[關鍵詞,紅變綠,紅不能繼續紅]
3、You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
只允許編寫剛好能夠使一個失敗的unit test通過的產品代碼
你所編寫的產品代碼應該以剛好能夠使得unit test編譯通過或者測試通過為準 [關鍵詞,切勿寫過多的產品代碼]
以TDD為榮,以不寫測試為恥。以高測試覆蓋率為榮,以低測試覆蓋率為恥!
宣傳語
歷經兩個半月的準備,三次大改版,十七次小改版。le1024終于要和大家見面了。
le1024每天推薦1~3段,有趣、有愛、有故事的視頻。
為您工作、學習、生活之余增加一點快樂的感覺。程序員必看的快樂視頻網站
參考:
書籍:《測試驅動的藝術》
https://github.com/burke/zeus
https://github.com/rails/spring
https://github.com/sporkrb/spork-rails
https://github.com/sporkrb/spork
http://railscasts.com/episodes/285-spork
http://www.adomokos.com/2011/04/running-rails-rspec-tests-without-rails.html