這篇文章主要討論一下幾點(diǎn)問題
- 如何開始寫一個(gè)單元測(cè)試
- 單元測(cè)試有我自己的一些實(shí)踐
這篇文章的假設(shè)為你明確自己要寫單元測(cè)試了,如果您不符合這個(gè)假設(shè),可以參看這篇文章 先解決思想:為何要寫單元測(cè)試
當(dāng)打算開始寫單元測(cè)試時(shí)。你調(diào)整了下坐姿,氣運(yùn)丹田,感覺到冥冥之中又向高質(zhì)量代碼邁進(jìn)了一步,但當(dāng)你的手下意識(shí)的敲擊鍵盤的時(shí)候,又覺得似乎哪里不太對(duì)勁:“恩,應(yīng)該怎樣開始寫一個(gè)單元測(cè)試呢?”
如何寫單元測(cè)試
首先我們需要明確,什么叫做單元測(cè)試。
在計(jì)算機(jī)編程中,單元測(cè)試(英語:Unit Testing)又稱為模塊測(cè)試, 是針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來進(jìn)行正確性檢驗(yàn)的測(cè)試工作。 程序單元是應(yīng)用的最小可測(cè)試部件。
我的理解是:測(cè)試某個(gè)具體的函數(shù),是否符合編寫者的預(yù)期。
其實(shí)也很好理解,就是將你編寫某個(gè)函數(shù)的功能與你的預(yù)期做一個(gè)比較,如果函數(shù)運(yùn)行的結(jié)果與你的預(yù)期相符,則說明測(cè)試通過,反之則失敗。
舉個(gè)很簡(jiǎn)單的例子
class Person
attr_accessor :name, :gender
def initialize(name, gender)
self.name = name
self.gender = gender
end
def say_hello
puts "#{self.title} #{name} said hello."
end
def title
self.gender == "male" ? "Mr" : "Ms"
end
end
我想測(cè)試一下 title 這個(gè)函數(shù)是否符合我預(yù)期,于是我會(huì)這樣寫測(cè)試(假設(shè)使用Rails 原生的 test框架)
require 'test_helper'
class PersonTest < ActiveSupport::TestCase
test "should return title correctly" do
person = Person.new("Ji Cheng", "Male")
assert_equal "Mr", person.title
person = Person.new "Han Meimei", "Female"
assert_equal "Ms", person.title
end
end
語言不同,測(cè)試框架不同都會(huì)導(dǎo)致代碼不同,但是思想都是一樣的,都是去 assert
一個(gè)值,與運(yùn)行后的函數(shù)值保持一致。
也許有人會(huì)說你這個(gè)函數(shù)太簡(jiǎn)單了,簡(jiǎn)直不用看就知道會(huì)發(fā)生什么,為什么還要寫個(gè)測(cè)試? 其實(shí)想想,寫出這個(gè)測(cè)試也花多長(zhǎng)時(shí)間,更重要的是,你在寫這個(gè)測(cè)試的時(shí)候,會(huì)更加清楚這個(gè)函數(shù)輸入與輸出,是否滿足預(yù)期,以及多一次使用自己寫的函數(shù)的機(jī)會(huì),體諒下調(diào)用你寫的函數(shù)的人。
細(xì)心的朋友肯定已經(jīng)看出來了,這個(gè)測(cè)試是會(huì)報(bào)錯(cuò)的,如果你真沒看出來,就更加說明單元測(cè)試的重要性。實(shí)際情況中太多函數(shù)是看不出來的,但是例如下面的 analysis_message
# encoding: utf-8
class JobStepService
attr_accessor :project, :job, :flow
# Some functions ...
class << self
def analysis_message(hash)
job_id = hash[:job_id]
index = hash[:index]
job_step = JobStep.find_by(job_id: job_id, index: index)
return if job_step.nil? || job_step.status == "stopped"
try_mark_last_job_status(job_id, index)
where = JobStep.where(job_id: job_id, index: index)
safe_update_job_hash(where, hash)
job_step.reload
end
private
def try_mark_last_job_status(job_id, index)
return if index.to_i.zero?
# 必須得找到, 不找到肯定是哪里出錯(cuò)了,應(yīng)該拋異常
JobStep.find_by(job_id: job_id, index: index.to_i - 1).update_attribute(:status, "success")
end
end
end
當(dāng)不是那么容易看出的時(shí)候,去寫一個(gè)單元測(cè)試是跟你在命令端調(diào)試所花的時(shí)間是差不多的。
class JobStepTest < ActiveSupport::TestCase
setup do
# do some initialize work...
end
test "could analysis message correctly" do
JobStepService.new(@job).generate_job_steps
assert_equal false, JobStep.count == 1
hash = { index: 0, status: "success", return_value: 0, log: "hello world\n", job_id: @job.id.to_s, category: "step" }
JobStepService.analysis_message hash
assert_equal "success", JobStep.asc(:index).first.status
assert_equal "pending", JobStep.asc(:index).last.status
hash = { index: 2, status: "failure", return_value: 0, log: "hello world\n", job_id: @job.id.to_s, category: "step" }
JobStepService.analysis_message hash
assert_equal "success", JobStep.asc(:index).to_a[1].status
assert_equal "failure", JobStep.asc(:index).to_a[2].status
end
想必大家也看出來了,測(cè)試甚至有些隨意不太友好,但是至少在跑了這段測(cè)試之后,我很信任之前寫的函數(shù)是沒有問題的(就算有,也不會(huì)是那些會(huì)被同事恥笑的低級(jí)錯(cuò)誤)。
寫單元測(cè)試一些實(shí)踐
大前提
所有的實(shí)踐前面都有一個(gè)大前提:首先你得寫單元測(cè)試。我非常喜歡寫一些顯而易見的單元測(cè)試當(dāng)做休息放松,當(dāng)別人問我為什么寫這種測(cè)試的時(shí)我通常是以“增加代碼測(cè)試覆蓋率”來忽悠他們。(但是實(shí)際上還是有30%左右的概率會(huì)測(cè)出各種問題,包含各種語法錯(cuò)誤,誤觸某回調(diào)等奇怪的錯(cuò)誤校驗(yàn)不過,也許我就是一個(gè)粗心的人),這樣做還有另外一個(gè)好處,培養(yǎng)自己對(duì)每個(gè)方法都寫測(cè)試的習(xí)慣:連很簡(jiǎn)單的方法都寫了,那稍微復(fù)雜點(diǎn)的,簡(jiǎn)直不能忍。
誰來寫
開發(fā)來寫。單測(cè)主要測(cè)試的是具體的函數(shù),沒有比開發(fā)人員更熟悉自己寫的函數(shù)了,同時(shí)本著“吃自己的狗糧”的原則,也可以反省下自己設(shè)計(jì)的函數(shù)是否合理。最重要的,當(dāng)自己寫完一個(gè)的時(shí)候,就可以把單元測(cè)試當(dāng)做自己手動(dòng)調(diào)試代碼,這樣就可以很自然的無縫的將單測(cè)寫上,而不用等測(cè)試人員排隊(duì)做。
關(guān)于測(cè)試覆蓋率
雖然這個(gè)東西聽起來很虛,但我覺得是個(gè)必需品,必須得上。當(dāng)有一個(gè)標(biāo)準(zhǔn)去衡量自己的工作進(jìn)度的時(shí)候,潛意識(shí)中大家會(huì)努力的提高這個(gè)指標(biāo)。同時(shí)絕大多數(shù)測(cè)試覆蓋率統(tǒng)計(jì)工具,都能通過界面顯示出你函數(shù)中未覆蓋的邏輯,避免自己漏測(cè)。我自己使用simplecov
這個(gè)gem 來統(tǒng)計(jì)我自己的Rails 項(xiàng)目的測(cè)試覆蓋。
寫的測(cè)試跑著要快
我非常贊同,寫的測(cè)試越慢,由于人的惰性,會(huì)導(dǎo)致自己因?yàn)椴幌氲忍枚慌軠y(cè)試。測(cè)試寫的再多,不跑全是白搭。
其實(shí)一個(gè)單元測(cè)試的內(nèi)容很少,那么一般慢是慢在哪里呢?
我覺得有以下方面
- IO
- sleep/wait 語句
- 數(shù)據(jù)庫(kù)的大量寫入
關(guān)于IO
目前我遇見比較多的是關(guān)于網(wǎng)絡(luò)的IO, 有些第三方組件會(huì)接入網(wǎng)絡(luò),這種一般都會(huì)帶來500ms左右的延時(shí),運(yùn)氣不好連國(guó)外(比如我們的項(xiàng)目連github API)沒準(zhǔn)就會(huì)變成假摔(一定概率的跑出錯(cuò),非必現(xiàn)的錯(cuò)誤)。常見的操作是 Stub 解決問題,各大語言都有很成熟的解決方案
。比如我現(xiàn)在使用的 webmock
這個(gè) gem ,當(dāng)然以ruby 這種 “開放式” 語言的能力,就算不引入任何gem,寫個(gè)猴子補(bǔ)丁也會(huì)非常的輕松。
關(guān)于sleep/ wait
大多數(shù)使用sleep/ wait 的時(shí)候都是在等待某個(gè)異步方法的執(zhí)行完成,我的處理方式是將異步的處理以及等待后面的語句都抽成兩個(gè)獨(dú)立的函數(shù),分別測(cè)試這兩個(gè)函數(shù),從而避免走sleep 這種慢的操作
關(guān)于數(shù)據(jù)庫(kù)
很多測(cè)試相關(guān)的文章和書籍都強(qiáng)調(diào) 數(shù)據(jù)庫(kù)太慢了,所以不能使用數(shù)據(jù)庫(kù)。我不太認(rèn)同,因?yàn)槠鋵?shí)很多時(shí)候?qū)懙拇a都需要依賴數(shù)據(jù)庫(kù)的一些特性,或者離開數(shù)據(jù)庫(kù)而存在內(nèi)存中會(huì)很麻煩(比如查詢語句,脫離數(shù)據(jù)庫(kù)mock 個(gè) where 很麻煩)。我的策略(當(dāng)然是Rails 的策略)是使用專門用于測(cè)試的數(shù)據(jù)庫(kù),每當(dāng)運(yùn)行一個(gè)單側(cè)的時(shí)候就會(huì)把它清除掉。這樣,測(cè)試數(shù)據(jù)庫(kù)的數(shù)據(jù)會(huì)非常的少,查詢、新增起來大多數(shù)情況下其實(shí)也在20ms以內(nèi)。
我是非常反對(duì)當(dāng)一個(gè)單元測(cè)試跑完后,不清除數(shù)據(jù)庫(kù)的,可能這些數(shù)據(jù)會(huì)影響到其他單元測(cè)試,進(jìn)一步造成了測(cè)試的假摔,假摔是大忌,應(yīng)該盡量避免。當(dāng)然清數(shù)據(jù)庫(kù)也不是絕對(duì)的,需要自己靈活判別,比如下面的情況。
我運(yùn)行測(cè)試之前會(huì)生成100條左右的模板數(shù)據(jù),這些數(shù)據(jù)是我在進(jìn)行單元測(cè)試的時(shí)候絕對(duì)不會(huì)操作的,所以沒必要每次執(zhí)行一個(gè)單元測(cè)試刪除再新建。但是為了防止我自己有時(shí)候沒想清楚改掉模板數(shù)據(jù)從而有可能造成假摔,所以我在執(zhí)行每個(gè)單元測(cè)試之前會(huì)判斷下這些模板數(shù)據(jù)的行數(shù)是否是我最初的生成的行數(shù)。
單元測(cè)試不是萬能的
會(huì)有人覺得我花了那么大的功夫,覆蓋率90%了,上 jenkins 或者 flow.ci 了,那我的程序就很穩(wěn)定了。這種觀念當(dāng)然是不對(duì)的,就如同你買了一把200塊的鎖就指望自己的自行車永遠(yuǎn)不會(huì)被偷一樣。良好的單元測(cè)試會(huì)極大的提高程序穩(wěn)定性,但是不會(huì)百分百的保證程序一定ok,畢竟人無完人。
從入門到放棄?
相信很多朋友其實(shí)也寫過單測(cè),但或因需求變更過快,或因一次次的失敗無力解決,導(dǎo)致了最終沒有堅(jiān)持下來。這當(dāng)中其實(shí)是有一定技巧的,使用良好的技巧會(huì)在保證測(cè)試覆蓋率的同時(shí),降低測(cè)試失敗的頻率。下次就來說說 如何使用一些技巧,讓我們?nèi)菀讏?jiān)持執(zhí)行這個(gè)應(yīng)該堅(jiān)持的單元測(cè)試。
最后,有興趣的朋友可以關(guān)注一下“持續(xù)集成慢慢來”這個(gè)公眾號(hào)與我交流,如果對(duì)持續(xù)集成感興趣,也可以試試我司基于SaaS的持續(xù)集成產(chǎn)品 flow.ci。