今天早上8點半坐到桌子前,開啟電腦,看了幾分鐘體育新聞,做其他一些準備工作,到9點開始真正開始著手寫這篇文章。於是開始google,找資料,打算列一大段冠冕堂皇的理由,來說明為什麼要寫單元測試,比如:
- 對軟體質量的提升
- 方便重構
- 節約時間
- 提升程式碼設計
- ……
等等等等。
然而我發現上面提到的幾點,都不是很好解釋。首先,我並沒有具體的資料,來說明有了單元測試,我們的app crash率降了多少,bug少了多少等等。這種東西首先我們沒有去衡量,因為單元測試的增加是循序漸進的,每個版本的迭代增加一點點。很難,我們也沒有,去前後對比。再次,crash率的降低和bug的減少,也難以證明就是單元測試的作用。另外,像重構這種理由,怎麼舉例證明呢?例子小了顯得沒有意義,例子大了寫起來很困難,讀起來也困難。而關於節約時間,我也沒有測量過,這個恐怕也很難去測量。只能從理論上去說明,為什麼可以節約時間,恐怕也很難有說服力的去論述。同樣的,對於程式碼設計的提升,也很難有力的去證明。
更重要的原因是,上面提到的種種好處,好像其實並不是我之所以要寫單元測試的直接原因,更多的,他們像是一種結果。所以如果從列舉和證明單元測試的好處這個角度去說明為什麼要寫單元測試的話,我感覺甚至很難說服我自己。
那就從自身的經歷和感受去說說,我為什麼要寫單元測試吧。其實我之所以要寫單元測試,或者說這麼喜歡單元測試這種寫程式碼的方式,是出於我自身的原因,或者說因為自身的一些缺點,讓我走上了單元測試這條路,而且再也不想回頭。
我為什麼寫單元測試
首先,是因為我不夠自信
我相信大家都有接手,或者說參與到一個新專案的經歷,也許是因為換了工作,也許是因為職位調動,或其他原因。當我拿到一個新專案的時候,會有一種誠惶誠恐的感覺,因為一時間比較難理清楚整個app的結構是怎麼劃分的,各部分各模組之間又是什麼樣的關係。我怕我改了某一個地方,結果其他一個莫名其妙的地方的受到了影響,然後導致了一個bug。這對於使用者群大的app,尤其嚴重。所以,那種時候就會希望,如果我改了某個地方,能有個東西告訴我,這個改動影響到哪些地方,這樣改是不是有問題的,會不會導致bug。雖然我可以把app啟動起來,看看是不是能正常工作,然而一種case能工作,並不代表所有影響到的case都能工作。尤其是在不知道有哪些地方用到了的情況下,我更加難以去遍歷所有用到的地方,一個一個去驗證這個改動有沒有問題。哪怕我知道所有的case,這也是一個很痛苦很費時間的過程,而且很多的外部條件也很難滿足,比如說需要什麼樣的網路條件,需要使用者是會員等等。
在這種情況下,單元測試是才是最好的工具。首先,單元測試只是針對一個程式碼單元寫的測試,保證一個程式碼單元的正確性總比保證整個app的正確性容易吧?遍歷一個方法的所有引數和輸出情況總比遍歷一個app的所有使用者場景容易吧?跑一次單元測試總比執行一次app快吧?
因此,在改現有的程式碼之前,我會先對要改的程式碼單元做好隔離,寫好測試,再去改,改好以後跑一邊單元測試,驗證他們依然是通過的,這時候我才有信心,將程式碼合併進去。
同樣的情況會發生在重構的時候,我是一個對爛程式碼不大有忍受能力的人,看到不好的程式碼,我會忍不住想要去重構,不然的話,沒有辦法寫新的程式碼。而重構就會有風險。因為我不夠自信,重構的時候,也會有一種誠惶誠恐的感覺。這時候如果有完備的單元測試的話,我就能知道我的這次重構到底破壞了哪些地方,是不是對的,這樣相對來說,就會放心的多了。
因此,想用單元測試來保證程式碼的正確性,這個是我喜歡寫單元測試的重要原因之一。
再次,是因為我沒有耐心
對於有一定經驗,有一定程式碼思想的人來說,當他拿到一個新的需求,他會先想想程式碼的結構,應該有那些類,那些元件,什麼責任應該劃分到哪裡去,然後才開始動手寫程式碼,這個是很自然的一個思維過程。然而在不寫單元測試的情況下,我們可能要把整個feature都做完整,從model到controller(或Presenter、ViewModel)到view到util等等,一整套流程做下來,到最後才可能執行起來看看是不是對的,有的時候哪怕所有程式碼都寫完了,也不一定能驗證是不是對的,比如說後臺還沒有ready等等。總之,在沒有單元測試的情況下,我們需要等到最後一刻才能手動驗證程式碼是不是對的,然後發現原來這裡錯了一點,那裡少了一點,然後一遍一遍的把app執行起來,改一點執行一遍。
當我開始寫單元測試之後,我發現這個過程實在是太漫長了,我喜歡寫完一部分功能獨立的程式碼,就能立刻看到他們是不是正確的。如果不是的話,我可以立刻就改正,而不用等到所有程式碼都寫完整。要達到這點,那就只有寫單元測試了。
當然,哪怕有單元測試,最後還是要做一遍手動測試工作,然而因為前面我已經保證每一個單元都是對的,最後只不過是驗證每一部分都是正確的串聯起來了而已,這點相對來說,是很容易的。所以最後所需要的手動測試,可以少很多,順利很多,也簡單得多。
最後,是因為我懶
如前所述,如果沒有單元測試的話,那就只有手工測試,把app執行起來,如果有錯的話,改一點東西,再執行起來。。。這個過程太漫長太痛苦,對於一個很懶的人來說,如果能寫程式碼來代替手工測試,每次寫完程式碼只需要按一次快捷鍵,就可以直接在IDE裡面看到結果,那是多爽的一件事!所以衝著這點,我也不想回頭。
我記得上一次使用“把app執行起來”這種開發方式,還是因為除錯一個動畫效果。因為動畫效果是很難單元測試的,那就只有改一點程式碼,跑一邊app,覺得不對,再改一點,跑一邊,這樣來來回回反反覆覆,那感覺真是。
單元測試給我帶來了什麼
前面講了為什麼我要寫單元測試的原因,接下來講講用了單元測試這種寫程式碼的方式以後,給我帶來什麼樣的好處。這根前面講的“原因”有部分重合的地方,然而也有不一樣的地方。
更快的結果反饋
這點前面講過了,有單元測試的幫助,我可以寫完一個獨立的程式碼單元,就立刻驗證它的正確性,這跟需要完成所有程式碼再把app執行起來手動測試相比,是一個更快的反饋迴圈,能更快的發現程式碼是否正確,也更快的得到一種成就感。
更少的bug,或者說更快的發現bug
正如上面所說,我們沒有做這樣的前後統計,來證明有了單元測試以後,我們app的bug少了多少。然而,我自己的經驗是,我已經不知道多少次以為只是做了一點小改動,不會有任何問題,結果一跑單元測試,發現還是改出問題來了。從這點來說,單元測試幫助我發現了不少問題,至少是更快的發現了問題。很多時候,這些問題是因為不小心疏忽了而導致的。然而話說回來,大部分bug不都是因為不小心疏忽了,很多情況考慮到,或者是考慮錯了而導致的嗎?
你或許會覺得,自己很厲害很專業,一定不會有這種“疏忽”,寫的程式碼一定是沒有bug的。然而事實是,再厲害的人,都有狀態不好的時候,都有情緒不高的時候,都有感覺比較累的時候,都會受到或多或少外界的干擾,這種時候都是很容易犯錯的。這個跟厲不厲害,專不專業其實沒有關係。李世石多麼專業,在跟AlphaGo比賽的時候,不是依然會失誤,會犯錯嗎?這個時候如果有那麼一層保障,來防止你不小心犯錯,豈不是更好的一件事情?
節約時間
對於安卓開發來說,一遍一遍的執行app,再執行相應的使用者操作,看介面是否顯示正確的結果,通過這種方式來測試自己的新程式碼、重構是否是正確的,這是非常浪費時間的一件事情,而且效果還不好。有了單元測試,我現在開發過程中幾乎已經不用把app執行起來了,速度相對來說快多了。
此外,因為單元測試能幫我減少bug,從而也減少了除錯bug,fix bug的時間。一個切身感受是,自從開始寫單元測試以後,我啟動AndroidStudio的debugger的次數明顯減少了。這也是單元測試節約時間的地方。
當然,這個結論也是自我感覺的結果。寫單元測試需要時間,這也是不能否認的事情,至於有單元測試是否真的更快,快了多少,我沒有具體的統計資料,所以很難給出一個確切的答案。
這裡需要重點說一下的是,你為新程式碼寫的單元測試,不僅僅是能在目前你這次寫新程式碼的時候起了作用,它的作用更體現在以後重構程式碼的時候,你可以很快速,很安全的進行重構。這點往往大家會忽略,所以會覺得在單元測試上花費的時間“不值得”。
更好的設計
當你為自己的程式碼寫單元測試的時候,尤其是採用TDD的方式,你會很自覺地把每個類寫的比較小,功能單一,這是軟體設計裡面很重要的SRP原則。此外,你能把每個功能職責分配的很清楚,而不是把一堆程式碼都塞到一個類裡面(比如Activity)。你會不自覺的更偏向於採用組合,而不是繼承的方式去寫程式碼。這些都是很好的一些程式碼實踐。
至於為什麼TDD能夠改善程式碼的設計,網上有很多的文章去分析和論證這個結論。我看到比較印象深刻的一句話是(具體在哪看的搜不出來了):當你TDD的時候,你是從一開始,就從一個程式碼的使用者,或者說維護者的角度,去寫你的程式碼。這樣寫出來的程式碼,自然會有更好的設計。
更強的自信心
有單元測試來保證你的程式碼是對的,這對於你寫程式碼、釋出程式碼、重構都提供了信心保證,沒有那麼多的擔心,從而工作起來也更快樂更開心。做人吶,最重要的是開心。
沒有時間寫單元測試?
前面大概講了講我為什麼要寫單元測試,以及單元測試給我帶來的好處,這些其實如果大家去google “why unit testing”,估計會得到類似的答案,然而依然會有很多人不寫單元測試。如果問為什麼的話,那麼得到最多的回答,估計是:沒有時間。
那麼,寫單元測試真的需要很多時間嗎?為什麼多數真正寫過單元測試的人會說,寫單元測試可以節約時間呢?在這裡,首先要承認兩點。
1. 單元測試,的確是一門需要學習的技術。
不僅需要學習,而且你要學習的東西還真不少,你要學習JUnit的使用,你要學習Mokito的使用,Robolectric的使用,依賴注入的概念和使用等等等待。此外,在剛開始的時候,你的確也會遇到很多坑,現有程式碼的坑,Android的坑,Robolectric的坑等等。這個在安卓開發這邊顯得更是如此,因為Android開發環境是公認的最不利用寫單元測試的環境之一。你需要花一些時間去學習如何處理,或者是繞過這些坑。
2. 在一個現有的,沒有單元測試的專案裡面加入單元測試,會需要一段時間的調整。
一個有單元測試的專案,跟一個沒有單元測試的專案相比,結構會有比較大的不同。因此剛開始,你會發現各種不順利的情況,你需要去調整各部分的程式碼,讓他們變的容易測試,這也是比較花時間的地方。
這種調整值得嗎?我認為是值得的,因為容易測試的專案,往往意味著更靈活,更具備擴充套件性,這個前面已經提到過了。所以本身這件事情就是一件值得做的事情,更何況,測試本身又是一件非常有價值的事情。
然而等跨過了這兩道坎,單元測試還需要花很多時間嗎?根據我自己的經驗,我覺得其實不是這樣的,因為等你熟悉瞭如何寫單元測試以後,要對一個類、一個介面寫單元測試,是很容易的一件事情。如果你發現一個類不好測,往往是因為這個類的設計是有問題的。此外,你可以慢慢的搭建自己的一套測試框架,簡化一些常用的繁瑣的寫法,讓寫單元測試變得更簡便快捷。再加上前面講述的原因,總體來說,我覺得寫單元測試非但不會需要跟多時間,反而會節約時間。
小結
這篇文章簡單講述了,為什麼要寫單元測試。其實,單元測試的必要性,看看那個知名的《國外程式設計師推薦:每個程式設計師都應讀的書》就知道了。在前20本中,所有5本講述“如何寫出更好的程式碼”的書,無一例外都強調單元測試的必要性。
- Code Complete
- The Pragmatic Programmer
- Refactoring: Improving the Design of Existing Code
- Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin
- Working Effectively with Legacy Code by Michael C. Feathers
希望這篇文章,能讓你多一點學習和實踐單元測試的決心,因為這真的是非常值得擁有的一項技能,只是剛開始的時候,需要多一點點時間而已。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!