當前位置:
首頁 > 科技 > 為什麼好的程序員會寫出糟糕的單元測試?

為什麼好的程序員會寫出糟糕的單元測試?

恭喜你!在寫了無數行代碼之後你終於可以買一套海景別墅了。你雇了世界著名的摩天大樓建築師 Peter Keating,他向你保證他設計的海景別墅是最好的。

幾個月後你終於迎來了剪綵的時刻。新房子是一棟人見人愛的鋼筋混凝土結構的五層大樓,上面覆蓋了閃閃發光的玻璃。你走過旋轉門,沿著地上的沙子踏上了豪華大理石鋪成的地板。在樓內,你看到了一個接待前台,後面還有電梯間,但樓上的主卧和三個次卧是只有辦公室格子一般大小的四個互相挨著的房間。

而我們的建築師 Peter Keating 不知道你為什麼不高興。「我遵循了所有最佳實踐。」他信誓旦旦地說。牆壁是三尺厚的磚,因為結構堅固最重要。因此,你的房子要比周圍的那些清爽宜人的小房子好得多。你也許沒有面向大海的大落地窗,但他說,那種窗戶不是最佳實踐,它們只會無謂地浪費能源,而且還會分散辦公室員工的注意力。

很多時候,軟體開發者在構建單元測試時有著同樣的想法。他們在產品代碼里機械地使用各種「規則」,而根本不關心這些規則是否適合他們的測試。結果就像在沙灘上蓋的這棟摩天大樓一樣。

測試代碼跟其他代碼不一樣

產品代碼的核心是抽象。好多產品代碼會將複雜性隱藏在精巧地劃分好的函數和類層次結構中。這樣閱讀者就可以很容易地瀏覽整個大型項目,而且還可以隨心所欲地查看更多細節,或者查看高層次的抽象。

而測試代碼完全不同。測試中的每一層抽象都會讓加大閱讀的難度。測試是診斷工具,所以很明顯應該盡量簡單明了。

好的產品代碼有好的結構;好的測試代碼非常簡明。

比如一把尺子。幾百年來尺子的形狀沒有任何變化,因為這種形狀很簡單,而且易於理解。假設我發明一種「抽象單位尺」,這種尺子需要另一張轉換表才能把「尺子的單位」轉換成英寸或厘米。

如果把這種尺子交給木匠,他們一定會把尺扔到我臉上。給一個簡單明了的工具增加一層抽象是非常荒謬的行為。

好的測試代碼也是一樣。它應當提供清晰的結果,而讀者不需要在多層之間跳來跳去。開發者通常對這一點有誤解,因為這一點跟寫產品代碼是不一樣的。

好的開發者也會寫出爛測試

我經常看到其他天才程序員寫出下面的測試:

這段測試是幹什麼的?它從名為 joe123 的用戶中取出「score」然後驗證分數為150。看到這裡你一定會有以下問題:

joe123 賬號從哪兒來的?

為什麼 joe123 的分數應該是 150?

很可能答案在 setUp 方法中,這個方法會在每個測試函數執行之前被調用:

好吧,setUp 方法創建了 joe123 用戶,其分數為 150,這解釋了為什麼 test_initial_score 期待這些值。那麼現在這個測試應該沒問題了吧?

你錯了,它依然是個爛測試。

別讓讀者離開測試函數

在編寫測試代碼時,應當考慮到別人可能需要處理該測試失敗的情況。他們絕不希望閱讀整個測試套件,肯定也不想閱讀一大堆測試工具的整個繼承樹。

如果測試失敗,閱讀者應當只需從頭到尾閱讀一邊測試代碼就能診斷問題。如果不得不去參考輔助的測試代碼,這個測試用例就沒寫好。

考慮到這一點,上一節的測試用例應當寫成這樣:

我只是將 setUp 方法中的代碼內聯到了測試函數中,但整個情況都不一樣了。現在,任何閱讀者都只需要閱讀該測試本身就能理解。它也遵循了「計劃-行動-斷言」的結構,讓測試的每個階段都十分明顯。

理想狀態是,閱讀者無需閱讀測試函數之外的代碼就能看懂。

不要害怕違反 DRY 原則

代碼內聯對於一個測試來說沒有問題,但要是有多個測試怎麼辦?這樣豈不是每次都需要重複相同的代碼嗎?坐好了,因為我要開始宣揚複製粘貼編程了。

這裡是同一個類中的另一個測試。

從 DRY 原則(Don"t Repeat Yourself - 不要重複)的角度來看,上面這段代碼非常糟糕。顯然裡面有重複代碼,我從前一個測試中直接複製了 6 行代碼過來。更不可思議的是,我認為上面這段違反 DRY 原則的測試要比前面沒有重複代碼的測試更好。這怎麼可能?

最理想的情況當然是不重複任何代碼實現清晰的測試,但別忘了不重複是手段,不是目的。目的是清晰簡單的測試。

在盲目應用 DRY 原則之前,仔細考慮下當測試失敗時怎樣才能更容易地找到問題所在。重構能減少重複,但也會增加複雜度,而且可能在測試失敗時讓信息更混亂。

如果一定的代碼重複能讓測試保持簡單,那就接受它。

添加輔助方法之前要三思

也許可以給每個測試都複製粘貼 6 行代碼,但是如果 AccountManager 需要更多的配置代碼該怎麼辦?

上面整整 15 行代碼的目的只是獲得 AccountManager 的實例然後測試它。在這個層次上,樣板代碼過多,分散了測試行為的注意力。

一個很自然的想法是把這一段代碼移到輔助方法內,但首先要問一個極其重要的問題:這樣會讓系統更難測試嗎?

過多的樣板代碼通常是弱結構的象徵。例如,上面的測試代碼表現出了多個設計異味(https://en.wikipedia.org/wiki/Design_smell):

AccountManager 直接訪問了 user_database 資料庫,但它的下一個參數是 privilege_manager,是對 privilege_database 的一個封裝。為什麼它要同時操作兩個不同層次的抽象?而且這跟 URL downloader 有什麼關係?後者顯然跟前兩個參數完全無關。

在這種情況下,重構 AccountManager 才能解決根本問題,而添加輔助方法只是掩蓋表象而已。

在嘗試寫輔助方法之前,先嘗試重構產品代碼。

如果真需要輔助方法,就要負責地寫好

然而,很多時候你並不能為了可測試性就隨便修改產品代碼。有時候,輔助方法是唯一的選擇,所以在需要輔助方法時要認真負責地寫好。

優秀的輔助方法需要秉承「把閱讀者留在測試函數內」的理念。只要不給閱讀者理解測試增加難度,那麼將樣板代碼放到輔助函數里也是可取的。

具體來說,輔助函數不應該:

埋藏關鍵值

與被測試的對象交互

下面的輔助方法的例子違反了上述原則:

閱讀者無法理解最終分數為什麼是175,除非他去閱讀輔助方法中隱藏的150。輔助方法還隱藏了 add_account 的調用,而不是把它留在測試函數內部,從而使得 account_manager 的行為更難以理解。

下面是修改後的例子:

它依然在輔助方法中隱藏了值,但這些值與測試無關。它還將 add_account 回調函數放在了測試函數中,這樣閱讀者可以很容易追蹤 account_manager 的情況。

必須保證輔助方法中不含任何閱讀者必須理解的信息。

不要懼怕使用長測試名

在產品代碼中下列哪個函數名更好?

userExistsAndTheirAccountIsInGoodStandingWithAllBillsPaid

isAccountActive

前者雖然能傳遞更多的信息,但它的長度達到了 57 字元,是個不小的負擔。許多開發者願意犧牲一部分準確性來換取簡潔但還可以接受的名字,如 isAccountActive(不包含 Java 開發者,因為對於他們來說上面的兩個名字都極其簡潔)。

但對於測試函數來說,一個殘酷的事實打破了這種平衡:測試函數永遠不會被調用。每個測試函數名僅需寫一次——那就是在函數簽名中。考慮到這一點,雖然簡潔依然重要,但遠不如在產品代碼中那麼重要。

而當測試失敗時,你首先看到的就是測試函數名,因此它應該傳達儘可能多的信息。例如下面的產品代碼:

假設你的測試套件運行後產生了如下結果:

你知道測試為什麼失敗嗎?估計不能。

TestNextToken的失敗告訴你NextToken()方法里出了問題,但對於一個僅有一個公有方法的類來說這並沒有什麼用。你還是要閱讀測試代碼才能診斷錯誤。

相反,如果你看到的是下面的信息情況又如何呢?

在其他語境中,ReturnsNullptrWhenStreamIsEmpty 這個函數名顯然太啰嗦了,但它卻非常適合測試。只要在測試失敗中看到它,就能立即明白類在處理空數據流時出錯了,很可能不需要閱讀測試代碼就可以去改 Bug。因此這才是好的測試名。

好的測試名應當具有描述性,讓開發者僅憑函數名就能診斷錯誤。

擁抱魔法數

「不要使用魔法數。」

這句話相當於是編程界的「不要跟陌生人說話」。許多有經驗的開發者都極力推崇這一點,他們絕不會認為魔法數會改善代碼。

你還記得魔法數是什麼嗎?魔法數就是代碼中出現的不含任何說明信息的數值或字元串。例如;

程序員們都認為魔法數在產品代碼中非常糟糕,所以他們會用命名常量來代替:

不幸的是,通常人們誤以為魔法數也會減弱測試代碼,然而事實正好相反。

看看下面的測試:

如果你認為魔法數皆邪惡,那你應該很喜歡上面的代碼。72.0 和 8.0 都有命名常量,所以沒人會指責魔法數的問題。

但等一下,先暫時放棄你的信仰,嘗試下魔法數的禁果:

這段代碼更簡單,只需要一半的代碼行。而且更容易閱讀,讀者不需要在函數里東張西望地跟蹤命名常量。

每當我看到開發者在測試代碼中定義常量,我就知道他們又誤解了 DRY,或者是他們懼怕使用魔法數。然而,測試很少有定義常量的需要,這樣做只會讓測試更難懂。

不要在測試代碼中定義常量。直接使用魔法數就好。

注意:測試代碼引用產品代碼中導出的常量是沒問題的。不要在測試代碼中定義就行。

結論

如果想寫出優秀的測試代碼,開發者必須根據測試代碼的目的來作出工程上的決定。最重要的是,測試應當儘可能簡化,使用儘可能少的抽象。好的測試應該讓讀者立即明白測試的行為,並且無需離開測試函數就能診斷問題。

原文:https://mtlynch.io/good-developers-bad-tests/

作者:Michael Lynch,軟體工程師。

譯者:彎月,責編:屠敏

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

什麼樣的備份容災系統才真正適合雲化數據中心?|技術頭條
移動開發者如何更好地學習 React Native?|技術頭條

TAG:CSDN |