架構之路(三) 單元測試

自由飛發表於2015-09-18

======================

 

實事求是的講,寫《【野生程式設計師】:優先招聘的時候,是帶著情緒的。其後也有反思,是不是我杞人憂天了?尤其是下面開始的幾條評論,如“都是混口飯吃的不容易”,“何以內外之分,中華兒女非山傾河洩而不能一氣前指,千年亦是如此”等,讓我感覺可能是我過於敏感了。但隨後一些人長篇大論,讓我明白,這篇部落格還是有意義的。

想一想,招聘啟示裡,你們要求“計算機專業本科以上學位”,我“計算機專業相關專業文憑”優先;然後,你們就炸了!我們沒有歧視,你這才是歧視!你自卑你憤青你酸你難成大器……我無力反駁,只是想說,每個人的言行都是他心靈的鏡子。謝謝你們!

其實,我沒有想挑起科班/非科班之爭(雖然可能結果會超出我的預料),我的本意是想給“非科班”的同學鼓氣,緩解他們身上的壓力,讓他們看到希望,給他們力量,讓他們相信,完全可以在更艱苦的環境下自學成才,而且結果不會比“科班”的差!但你一定要委下身段踏踏實實的去學,一步一個腳印的去做,自卑自大爭吵辯駁都無助於你的成長。請牢記:言語沒有力量

另外,願意聽一句的“科班”同學,“無計算機專業相關專業文憑”優先,並非完全出於義憤。都是築基,你是名門大派用資源用丹藥堆出來的,他是一路苦修戰鬥領悟突破的,你覺得誰更有潛力?所以啊,放下那些虛榮驕傲,真正的去戰鬥吧!畢業三年以後,是沒人再看你的學歷的。

另外宣告一點,對老趙沒有任何意見,除了景仰。他針對的是培訓機構我完全明白,但仍然不能贊同。所以我說,“每一次看到這一段文字,我的心裡就會有一種難以言表的複雜情緒”,至於如何複雜,不是說了嗎?難以言表啊。

 

======================

 

好,心平氣和之後我們繼續討論技術問題。在帶隊的過程中,效能的問題還比較好解決,最消極的想法,“好啊,多一事不如少一事,你讓我不管還不簡單?”,但要求寫測試程式碼,那就炸鍋了!以我的經歷,“測試驅動”是一個最具爭議的話題,沒有之一。吹捧者和反對者涇渭分明,而且都有大量的論據和證明。記得部落格園曾經有一篇文章,大意是:“公司付錢給你不是讓你寫測試程式碼的”,下面一片狂贊。

 

在我自己的專案開始的時候,我是放棄了測試驅動的(呵呵,還找到了原文),裡面總結得很準確,最大的原因是“懶”。但最後讓我下定決心開始“測試驅動”實踐的,是我一次花了兩天一夜都沒調出一個Bug,垂頭喪氣筋疲力盡之後,無可奈何的接受了這個現實:測試還是很有用的——即使是自己寫的程式碼。我之前的系列部落格,也已經反覆的強調,架構是一種“無奈”,是現實是問題驅使你去做一些其實你本來不想做的事情。你無法理解一些看起來像“脫了褲子放屁”一樣的行為,通常只是因為你沒有遭遇過那些現實那些問題。(看看,大學能教你這些東西麼?)

 

即使你沒有多少開發經驗,你也應該能夠想象,單元測試最大的問題,就是它需要花時間花精力去寫,那麼這個花費是否值得呢?這還是由你架構的目標決定的,或者你的需求決定的。如果系統是一次成型交付使用,此後幾乎不會更改的,那麼一次性的手工測試就夠了;但如果你的系統是會被“千錘百煉”的不斷折騰修改的,那麼這個測試就是很有必要的。最簡單的考慮:每一次更改,我都要手工測試一次;那還不會如我多花點時間,弄個“自動化”的東西出來。單元測試,其實就可以理解為一種自動化的測試工具。

 

但是“自動化”的理由還遠遠不夠。因為你馬上想到的,每一次需求變更程式碼調整,測試程式碼也得相應的改呀?沒有測試程式碼,我就只需要改開發程式碼;現在有了單元測試,我還得再改測試程式碼。本來我只維護一套程式碼,現在我憑空增加了一套程式碼也需要維護,這不是增加了維護成本,不是和你“可維護性”的架構目標背道而馳了麼?是一套程式碼好維護呢,還是兩套程式碼更好維護?

 

這是一個非常好的問題,適用於很多情景(比如分層架構,你說分層解耦,實際上還不是一改就得從UI層改到資料庫,每一層都得改?)。我能給出的回答大概有:

 

一、無論有無單元測試,開發程式碼進行修改之後,是不是都要進行測試?沒有單元測試,並不代表你的程式碼就不需要測試了,只不過是你手工的去測試了一遍而已。切記:你的工作並不只是把程式碼寫出來而已

 

二、進行手工測試,和更改單元測試,兩者的耗費比,會根據測試重用的次數而變化。一次手工測試可能需要5分鐘跑完,更改單元測試程式碼可能需要20分鐘,但如果這測試會跑100遍,單元測試完勝手工測試。

 

三、你說,哪裡喲?什麼功能會改100遍?我沒說你的功能會改100遍,我說的是測試會跑100遍。有區別麼?你可能還在犯迷糊,是吧?好吧,我們講個故事。

 

有一個小夥子,他很不情願寫測試程式碼。老闆拿他沒轍啊,也沒那麼多精力和他磨牙,於是老闆自己寫單元測試。這小夥子的程式碼提交之前要review,老闆總能一次次的找出它程式碼的問題。他改的是登入,老闆告訴他積分系統被他改出了問題;他又去改積分,老闆又告訴他訊息通知系統被他改壞了;他又去改訊息系統,老闆告訴他登入還是有問題……於是他崩潰了,“這TM什麼一個爛系統”?最終他終於回過神來了,為什麼老闆總能知道這裡的改動會影響那裡呢?老闆的思維有這麼嚴謹?老闆躲在一旁偷笑,就不告訴你,“其實我就是跑了一遍單元測試而已”。

這個老闆就是我。我故意的,就不一次性的告訴他所有的問題,就要這樣一次次的折磨他,讓他的痛苦能刻入骨子裡去。最後,我還要問他:

  • 你現在對你的程式碼是不是還那麼自信?
  • 如果沒有我的review(我也是靠單元測試),你能不能發現這些問題?
  • 如果我們的專案已經部署到生產環境,而且你的改動帶來的破壞沒有被發現就上線了,會帶來什麼樣的後果?

這一次,他服氣了。後來他用NUnit用得麻溜麻溜的。每一次改動,如果有意想不到的未通過test case,他都會很激動的給我張截圖,順便發發牢騷。我微笑不語,那種滿屏綠燈通過的踏實,和意外爆出紅燈之後的驚喜,沒有經歷過的人,是無法體會的。

所以其實當物件間的關係變得越來越錯綜複雜,像一張密密麻麻的網一樣之後,一個區域性的改動就很有可能會觸發極其複雜的連鎖反應。所以為了保險起見,所有可能相關的元件都應該進行測試(所謂的“迴歸測試”)。這時候如果只有純粹的手工測試,會面臨兩個問題:

  • 難以確定測試的邊界(那些部分可能會被影響),這得我們腦袋憑空硬想啊,兄弟!
  • 極大的測試耗費。而且這種耗費是相當的無聊繁瑣傷人心的——沒人願意做這種事。據說所知,現在很多公司測試人員的工資已經比開發人員還高了。為什麼?簡單枯燥無聊,沒人願意做啊!

 

好的,我假設你已經認識到了單元測試的重要性,並開始摩拳擦掌,躍躍欲試。接下來我得給你潑一大瓢冷水:單元測試不是那麼好寫的!從某種程度上講,寫單元測試比寫開發程式碼還難。難得我工作的所有公司,沒有一家有過成功的案例。

 

大概是幾年前,我在公司修bug,老大告訴我,“你這個功能比較核心,跑一下單元測試吧”。

“哇塞!我們有單元測試?”一種高大上的感覺迅速瀰漫全身,終於見到傳說中的Unit了!

搗鼓了一會,能跑了,試試看——我的個媽呀?怎麼這麼多紅燈?我真被嚇住了,這都是我的改動造成的?

老大就是老大,不慌不忙,“數一下有多少個通不過?”

“啊?”我以為我聽錯了,數多少個通不過有什麼用?得把他們全部弄通過啊?!

搞了一會兒,才終於弄明白了,把我改動前後的程式碼分別跑一遍,對照一下通過失敗是不是一樣的,只要是一樣的,就OK了。比如,以前是8個通不過,現在還是8個通不過,這樣就可以了!

 

我一直不明白,為什麼不把那8個通不過的單元測試給弄成通過呢?這樣擺著究竟算什麼?直到我自己開始寫單元測試。坑爹啊!到處都是坑,跳出小坑進大坑,大坑下面還連著小坑,前面是坑後面是坑,一堆一堆的連環坑……

 

單元測試寫出來容易跑過難!而且跑不過的原因還不是你的開發程式碼邏輯錯了,而是測試環境/資料出問題。要測試,一定要有資料,這個資料的構建,完全不是我們所想象的那麼簡單。以我們創業家園專案裡的積分系統為例,假設一個簡單的需求:部落格被點贊,部落格的作者應該獲得一定積分,該積分數量是由點贊人目前所有的可用幣轉換而得來的(已簡化,具體可參考文件:積分)。要準備的資料就有:部落格一篇,要有作者,作者已有積分;點贊人一名,有一定數量可用幣。如果只是這樣,還可以接受,但其實下面會有一堆的問題:

  • 作者的積分從哪裡來?我們的開發程式碼,出於封裝的考慮,使用者的積分是隻讀的,你單元測試怎麼設這個值?
  • 要麼寫程式碼,模擬作者通過其他行為(釋出文章回答問題等)獲得積分,這將開啟新一輪噩夢;
  • 如果用Mock或者反射強行設定,事實上省略了作者獲得積分的歷史,所以使用者“積分歷史”為null,之後對其“加積分”時,就會報異常。
  • 更坑的是,你以為你什麼都處理好了的時候,你突然悲哀的發現,這個部落格得首先“被髮布”,而部落格一經發布,其作者就獲得了一定數量的積分,所以你以前設定的積分又變了!
  • ……
  • 點贊人的可用幣,同樣可能遇到類似的問題。可用幣怎麼設定,設定之後會不會在跑測試時被意外更改?
  • 點讚的行為,被封裝成一個方法,執行這個方法,會檢查點贊人之前是否已經對該文章點過贊,所以還應該有一個“點贊歷史記錄”,哪怕是空的,都得new一個,否則就空異常
  • ……

反正當時是寫得我直接摔了滑鼠!寫得憋屈啊!而且我還是完全隔絕了資料庫的,真不知道那些要從資料庫裡取資料來跑單元測試的,是怎麼做的?這時候我一下子就明白了,實際工作中加班趕進度,一個接一個的填坑,連重構的時間都沒有,怎麼可能還擠得出時間來寫單元測試?就算開始雄心勃勃的寫了,隨著系統日益複雜,維護單元測試的成本也與日俱增,甚至複雜度更甚開發,所以放棄也就成了絕大多數專案的唯一選擇。

 

在公司上班麼,大多數人都是這樣的,能推就推。我們開發寫完了程式碼,基本上能跑了,就該交給測試人員了呀!天經地義的嘛,是不是?而且測試的時間是不會計算到我的專案開發時間裡的,我總算是按時完成了開發任務。累壞了,休息一下,讓測試的忙活去吧,哈哈……

 

但我是個光桿司令,我沒測試人員啊!曾經有那麼一兩個時候,我真準備招一兩個測試人員的。但好在我天生的節儉美德(也就是“摳”啦)讓我冷靜下來。我就想啊:測試只能告訴你出了bug,不能告訴你根源啊。沒有單元測試,我單步除錯,不也折騰了兩天了麼?這是系統本身的複雜性,或者程式碼組織的不合理造成的,不能歸咎於單元測試。不還是有這麼多開原始碼都有詳盡的單元測試麼?他們是怎麼做到的呢?在單元測試上的付出,最終一定會獲得超值回報!想想沒有單元測試的公司,那超級龐大的測試團隊,或者四處冒煙的系統,你願意走這麼一條路麼?

 

所以我不斷的告誡自己,不要著急,冷靜細緻。終於一步步抽絲剝繭,把這一團亂麻一點點的歸納整理,最終還真被我找到了一條路子,一個個的單元測試都慢慢完成通過了,開發程式碼裡潛在的一些問題也浮出水面,被我一個個的消滅。最後再跑一遍單元測試,一路綠燈,哈哈!更奇蹟的是,困擾我兩天的bug不知道什麼時候消失了?

 

後來,我看到這樣一種說法:可測試的程式碼不一定是好程式碼,但壞程式碼幾乎是不可能被測試的。深以為然!深度耦合的程式碼,寫他們的單元測試,難於上青天;但反過來,我們可以以可測試為標準,不斷的完善重構開發程式碼,只要這樣堅持下來,最終程式碼的質量怎麼都不會差到哪裡去。

 

所以,於我而言,單元測試是否有價值的爭論可以休矣!不如換個角度,想一想,怎樣才能把單元測試堅持下去。

 

最後,如果有心的同學就會注意到,我一直用的是“單元測試”,而不是“測試驅動”。因為測試驅動是一個更廣闊的概念,是一個更嶄新的天地!單元測試只是其中的一小部分,在下一篇部落格,我會講解我是如何試著將測試驅動的概念運用到專案開發管理中去的。這裡,需要強調的一點:先寫測試

 

一上手就寫開發程式碼,寫完了才寫單元測試。這是很多開發人員的習慣,我也經常犯這樣的毛病,一不留神就忘了。這樣做最大的問題就是,沒有真正實現“測試驅動”。你實際上還是由開發在驅動,那麼很自然的,測試照著開發的if...else...寫一遍,有什麼意義呢?這樣做下去,就會不斷的強化“測試無用累贅”的印象,因為測試就是簡單的把開發程式碼重寫一遍而已。我開的藥方是:

  • 單元測試程式碼和開發程式碼由不同的人員編寫
  • 如果做不到上面一點,先寫單元測試
  • 如果連上面一點也做不到,直到出了bug了再寫單元測試

第三條可能有同學無法理解,不是說單元測試很重要麼?為什麼要等到出了bug才寫?答案是:偷懶唄!記住,我們程式設計師是世界上最懶的人,沒意義的事從來不做!你先寫開發程式碼再寫測試真的沒意義,沒意義就乾脆不要做了。但你可以開啟“樂觀模式”(或者“Lazy模式”?),先樂觀的認為,我的程式碼沒問題,或許真的就沒問題呢,是吧?如果真出了問題,做一個補救,這個時候就應該用單元測試把這個問題表現出來,因為他根據墨菲定律,它這裡出了問題,以後就很有可能繼續出問題。這個時候,就不要再偷懶了。

 

(未完待續)

 

老規矩,說說我的專案進度。

創業家園 裡新增了“個人資料”功能,可以展示個人所在城市、技能特長等資訊,修復了一些小bug。

創業家園 還沒空打理。大家不要再噴它的美工了,美工其實就是在下。以後(現在還沒用)採用bootstrap,應該會好看一些。

相關文章