寫單元測試簡直是傻,如果不符合預(yù)期,我就直接改我自己的代碼好了。
如果說領(lǐng)導(dǎo)讓研發(fā)寫單元測試,我敢打賭80%的研發(fā)腦海里都會想過這個問題。 我寫了一個函數(shù),這個函數(shù)的結(jié)果我當(dāng)然知道是什么,但是為什么我還要寫一個單元測試來確定這個事情?
我的答案是,再簡單的事情,都有可能出錯。
讓我們想想我們的工作中的情況在自己寫功能的時候,有多少情況是自己一次編寫就能確保這個函數(shù)能夠一次編譯通過的?隨著自己工作年限的增長,我寫的代碼越多,就越明白一個道理: 人是會犯錯誤的,無論這個錯誤有多么低級。
以下是一個非常簡單的例子。
class Member
def need_notication?
return false if %w(cancel paid).include? self.status
self.expire_date - 1.week > Time.now
end
end
如此簡單的代碼,程序員一眼就能看明白,那為什么我們要對如此簡單的代碼寫單元測試呢?
有一天下午,我暈暈乎乎的讀到這段代碼的時候,覺得這里的 expiration 的判斷有問題,于是乎,改為了
self.expiration_date + 1.week > Time.now
當(dāng)時比較忙,覺得改動很簡單,直接push代碼了
由于早期寫這個功能的時候順手寫了個簡單的單元測試
it "could send expiration notification correctly" do
member = build(:member, expiration_date: 5.days.since)
assert_equal true, member.need_notication?
member2 = build(:member, expiration_date: 1.month.since)
assert_equal false, member.need_notication?
end
push代碼后一兩分鐘,我們的CI系統(tǒng) flow.ci 就發(fā)郵件提示單元測試失敗了,打開郵件一看,原來是自己腦袋不清醒瞎改東西了。
會有人說,自己不會犯這個錯誤,其實我在清醒的時候也不會犯這種錯誤,但是我深知自己肯定會有不清醒的時候。
就像那句著名的概率學(xué)玩笑 “不可能事件隨著時間的累積是一定會發(fā)生的” 一樣,項目越大,越久,出現(xiàn)各種自己預(yù)想不到的錯誤越大。該大寫的小寫了、打字 "O" 結(jié)果按到 "0" 了等等問題,都會出現(xiàn),更別代碼提邏輯上的錯誤了。
當(dāng)然,很多種方式可以避免這種問題。大多數(shù)人使用工手動測也能發(fā)現(xiàn)類似的問題:在代碼中加 print, 或者在 IDE中debug自己剛寫的代碼,手動跑一跑就可以解決。
然而換個角度想想,為什么不能將“手動跑”這個過程代碼化過來呢?我將手動測試轉(zhuǎn)化為單元測試時間大概是原來手動測試的 2-3 倍。乍看起來還挺多的,然而卻解決了我另外一個問題:無論什么時候,我都能確保我的函數(shù)是測試過的
一般在上線的時候大家都會問,“所有功能測過了么?” 如果這次上線是一次大上線,積累了很多功能,我相信沒人能夠說“都測過了”,就算都測過了,也會因為時間間隔太久,而不敢確定這件事情。但如果自己寫過單元測試,則每次運行測試的時候,所有測試用例都會運行,會非常有底氣的說,都測過了。
更重要的是,當(dāng)項目的代碼發(fā)生變更的時候,僅僅跑一下測試,就能重復(fù)之前的測試動作,判斷修改的代碼是否影響到了之前的邏輯。特別是當(dāng)項目由很多人參與的時候,時常代碼的邏輯就被你不知情的改變了,而跑一遍單元測試,如果代碼的改變影響到了測試的邏輯,則測試就會failure,這時大家就可以充分的意識到修改的內(nèi)容影響到了哪些代碼了。
這一系列的好處,僅僅是比手動測試多出 2-3倍 的時間而已。遠(yuǎn)遠(yuǎn)低于在出問題后debug的時間。
很多時候,我們拒絕做出那些看起來無趣的事情,然而就是那些看起來很愚蠢,很繁雜的事情,卻會在你不經(jīng)意的時候,拯救于你。不信,你可以想想背包里的傘,以及錢包里的夾層。
確定引入單元測試后,肯定會有一些陣痛感,如何降低這些陣痛,提高單元測試的價值,下次寫 “小談單元測試寫法”。