編寫可測試的代碼

已發表: 2022-11-03

單元測試是任何軟件開發人員工具箱中必不可少的工具。 在處理遵循最佳軟件設計實踐、模式和原則的代碼庫時,編寫單元測試相對容易。 真正的問題出現在嘗試對設計不佳、無法測試的代碼進行單元測試時。

本博客將討論如何編寫更多可單元測試的代碼,以及避免哪些模式和不良做法以提高可測試性。

可測試和不可測試的代碼

在處理需要長期維護的大型應用程序時,我們必須依靠自動化測試來保持系統的整體質量高。 與將多個單元作為一個整體進行測試的集成測試相比,單元測試具有快速且穩定的優勢。 快速是因為我們正在實例化(理想情況下)只是被測試的類,並且穩定是因為我們通常會模擬外部依賴項,例如數據庫或網絡連接。

如果您不熟悉單元測試和集成測試之間的確切區別,可以在我們的測試簡介博客中閱讀有關此主題的更多信息。

可測試的代碼可以與我們代碼庫的其餘部分隔離開來。 換句話說,可以獨立測試最小的單元。 不可測試代碼的編寫方式很難,甚至不可能為它編寫一個好的單元測試。

讓我們回顧一下在編寫可測試代碼時應該避免的一些反模式和不良做法。

示例是用 Java 編寫的,但這裡提到的編碼約定適用於任何面向對象的編程語言和測試框架。 在這篇博文中,我們將使用 assertj 和 JUnit5 作為示例。

依賴注入

依賴注入是實現測試隔離最重要的設計模式之一。 依賴注入是一種設計模式,其中一個對象通過構造函數參數或設置器接收其他對象(依賴項),而不必自己構造它們。

通過依賴注入,我們可以通過模擬對象的依賴關係輕鬆隔離被測類。

我們來看一個沒有依賴注入的例子:

由於引擎依賴是在 Car 類的構造函數中構建的,因此可以說 Car 和 Engine 類是緊密耦合的。 他們高度依賴彼此——改變一個就需要改變另一個。

從測試的角度來看,您不能單獨測試 Car 類,因為上面的示例無法用測試替身替換具體的 Engine 實現。

但是,我們可以使用依賴注入和多態來實現隔離:

現在我們可以構建多個引擎實現,因此可以構建具有不同引擎的汽車:

現在可以進行隔離測試,因為我們可以創建 Engine 抽象的模擬實現並將其傳遞給我們的 Car 類:

在處理需要其他對象(依賴)的對象時,您應該通過構造函數參數(依賴注入)來提供它們,理想情況下隱藏在一些抽象之後。

通過遵循這種模式,您的代碼變得更具可讀性,並且可以隨著時間的推移而變化。 此外,您應該避免在構造函數中進行實際工作——除了字段分配之外的任何事情都是實際工作。 構造函數中的 `new` 關鍵字始終是不可測試代碼的警告標誌。

這裡要注意的一點是,在某些情況下,緊密耦合(例如,構造函數中的新關鍵字、用於邏輯隔離的內部類、對象映射器)並不是一個壞習慣——作為“獨立”類沒有意義的類。

全局狀態

共享全局狀態通常會產生不穩定的測試(有時通過,有時失敗),尤其是在多線程環境中。

想像一個場景,多個被測對象共享相同的全局狀態——如果其中一個對像中的方法觸發改變共享全局狀態值的副作用,則另一個對像中的方法的輸出變得不可預測。 避免使用不純的靜態方法,因為它們會以某種方式改變全局狀態或被代理到某些全局狀態。

我們來看看這個不純的靜態方法:

本質上,此方法讀取當前系統日期和時間並根據該值返回結果。 為這個方法編寫一個適當的基於狀態的單元測試是非常困難的,因為 LocalDateTime.now() 靜態調用會在我們的測試執行期間產生不同的結果。 如果不更改系統日期和時間,就不可能為此方法編寫測試。

為了解決這個問題,我們將日期時間作為參數傳遞給 timeOfDay 方法:

timeOfDay 靜態方法現在是純的——相同的輸入總是產生相同的結果。 現在我們可以輕鬆地將孤立的 dateTime 對像作為測試中的參數傳遞:

得墨忒耳法則

得墨忒耳定律,或最少知識原則,規定對象應該只知道與第一個對象密切相關的對象。 換句話說,一個對象應該只能訪問它需要的對象。 例如,我們有一個接受上下文對像作為參數的方法:

這種方法違反了得墨忒耳定律,因為它需要遍歷一個對像圖來獲取所需的信息來完成它的工作。 將不必要的信息傳遞給類和方法會損害可測試性。

想像一個巨大的 BillingContext 對象,其中包含對其他對象的引用:

正如我們所看到的,我們的測試充滿了無關緊要的信息。 創建複雜對像圖的測試很難閱讀並引入了不必要的複雜性。

讓我們修復之前的示例:

您應該始終將直接依賴項傳遞到您的類和方法中。 但是,將許多參數傳遞給方法也不是一個好習慣——理想情況下,您應該傳遞最大的兩個參數或將密切相關的參數包裝到數據對像中。

上帝的對象

上帝對像是一個引用許多其他不同對象的對象,具有多個職責,並且有多個更改原因。 如果總結班級所做的事情具有挑戰性,或者如果總結包含“和”這個詞,那麼班級可能有不止一項責任。

上帝對像很難測試,因為我們正在處理多個不相關的依賴關係,混合了不同級別的抽象和關注點,並且它們會產生很多副作用。 因此,我們的測試用例很難達到預期的狀態。

例如:

UserService 的職責不止一項——註冊新用戶和發送電子郵件。 在測試用戶註冊時,我們需要處理電子郵件服務,反之亦然:

想像一個 UserService 具有兩個以上不相關的依賴項。 這些依賴有自己的依賴,以此類推。 我們最終會得到一個不可讀的測試,其中包含不相關的信息並且非常難以理解。 因此,每個班級應該只有一個責任和改變的理由。 只有一個更改理由的類是稱為單一職責原則的五項軟件設計原則之一。

您可以在此處閱讀有關 SOLID 原則的更多信息。

結論

遵循軟件設計最佳實踐的代碼庫使編寫單元測試更易於管理。

另一方面,使用上述反模式為代碼庫編寫工作單元測試可能非常具有挑戰性,有時甚至是不可能的。 編寫好的可測試代碼需要大量的實踐、紀律和額外的努力。 可測試代碼最顯著的優勢是易於測試以及理解、維護和擴展該代碼的能力。

我們希望這個博客可以幫助您編寫可測試的代碼。