編寫重要的測試:首先處理最複雜的代碼

已發表: 2022-03-11

圍繞代碼質量主題有很多討論、文章和博客。 人們說 - 使用測試驅動技術! 測試是開始任何重構的“必備”! 這一切都很酷,但現在是 2016 年,還有大量的產品和代碼庫仍在生產中,這些產品和代碼庫是在十年、十五甚至二十年前創建的。 眾所周知,他們中的許多人都有測試覆蓋率低的遺留代碼。

雖然我希望始終處於技術世界的領先甚至前沿——參與新的酷項目和技術——但不幸的是,這並不總是可能的,而且我經常不得不處理舊系統。 我喜歡說,當你從零開始發展時,你就像一個創造者,掌握著新事物。 但是當你處理遺留代碼時,你更像是一名外科醫生——你知道系統一般是如何工作的,但你永遠不知道病人是否能在你的“手術”中倖存下來。 而且由於它是遺留代碼,因此沒有多少最新的測試可供您依賴。 這意味著最常見的第一步是用測試覆蓋它。 更準確地說,不僅僅是提供覆蓋,而是製定測試覆蓋策略。

耦合和圈複雜性:更智能的測試覆蓋率的指標

忘記 100% 的覆蓋率。 通過識別更有可能破壞的類來進行更智能的測試。
鳴叫

基本上,我需要確定的是我們首先需要測試系統的哪些部分(類/包),我們需要單元測試的地方,集成測試更有幫助的地方等等。無可否認,有很多方法可以處理這種類型的分析,我用過的可能不是最好的,但它是一種自動的方法。 一旦實現了我的方法,實際進行分析本身只需要最少的時間,更重要的是,它為遺留代碼分析帶來了一些樂趣。

這裡的主要思想是分析兩個度量——耦合(即傳入耦合或CA)和復雜性(即圈複雜性)。

第一個測量有多少類使用了我們的類,所以它基本上告訴我們一個特定的類與系統的核心有多接近; 使用我們的類的類越多,用測試覆蓋它就越重要。

另一方面,如果一個類非常簡單(例如只包含常量),那麼即使它被系統的許多其他部分使用,創建測試也沒有那麼重要。 這是第二個指標可以提供幫助的地方。 如果一個類包含大量邏輯,則循環複雜度會很高。

同樣的邏輯也可以反過來應用; 即,即使一個類沒有被許多類使用並且只代表一個特定的用例,如果它的內部邏輯很複雜,用測試覆蓋它仍然是有意義的。

但是有一個警告:假設我們有兩個類 - 一個具有 CA 100 和復雜性 2,另一個具有 CA 60 和復雜性 20。即使第一個的指標總和更高,我們絕對應該涵蓋第二個先。 這是因為第一個類被許多其他類使用,但不是很複雜。 另一方面,第二類也被許多其他類使用,但相對比第一類複雜。

總結一下:我們需要識別具有高 CA 和圈複雜度的類。 在數學術語中,需要一個可用作評級的適應度函數 - f(CA,Complexity) - 其值隨著 CA 和復雜度的增加而增加。

一般來說,兩個指標之間差異最小的類應該被賦予測試覆蓋率最高的優先級。

尋找工具來計算整個代碼庫的 CA 和復雜度,並提供一種以 CSV 格式提取此信息的簡單方法,這被證明是一項挑戰。 在搜索過程中,我發現了兩個免費的工具,因此不提它們是不公平的:

  • 耦合指標:www.spinellis.gr/sw/ckjm/
  • 複雜性:cyvis.sourceforge.net/

一點數學

這裡的主要問題是我們有兩個標準——CA 和圈複雜度——所以我們需要將它們結合起來並轉換成一個標量值。 如果我們有一個稍微不同的任務——例如,找到一個與我們的標準組合最差的類——我們將遇到一個經典的多目標優化問題:

我們需要在所謂的帕累托前沿(上圖中的紅色)上找到一個點。 帕累托集的有趣之處在於,集合中的每個點都是優化任務的解決方案。 每當我們沿著紅線前進時,我們都需要在我們的標準之間做出妥協——如果一個變得更好,另一個變得更糟。 這稱為標量化,最終結果取決於我們如何做。

我們可以在這裡使用很多技術。 每個都有自己的優點和缺點。 然而,最流行的是線性標量和基於參考點的。 線性是最簡單的。 我們的適應度函數看起來像 CA 和復雜度的線性組合:

f(CA, 複雜度) = A×CA + B×複雜度

其中 A 和 B 是一些係數。

代表我們優化問題的解決方案的點將位於這條線上(下圖中的藍色)。 更準確地說,它將位於藍線和紅色 Pareto 前沿的交匯處。 我們最初的問題並不完全是一個優化問題。 相反,我們需要創建一個排名函數。 讓我們考慮排名函數的兩個值,基本上是 Rank 列中的兩個值:

R1 = A∗CA + B∗Complexity 和 R2 = A∗CA + B∗Complexity

上面寫的兩個公式都是直線方程,而且這些直線是平行的。 考慮到更多的排名值,我們將得到更多的線,因此帕累托線與(虛線)藍線相交的點也更多。 這些點將是對應於特定排名值的類別。

不幸的是,這種方法存在問題。 對於任何一條線(等級值),我們都會有 CA 非常小且複雜度非常大(反之亦然)的點位於其上。 這會立即將度量值之間存在很大差異的點放在列表頂部,這正是我們想要避免的。

進行標量化的另一種方法是基於參考點。 參考點是兩個標準都具有最大值的點:

(最大(CA),最大(複雜性))

適應度函數將是參考點和數據點之間的距離:

f(CA,複雜度) = √((CA−CA ) 2 + (複雜度−複雜度) 2 )

我們可以把這個適應度函數想像成一個以參考點為中心的圓。 在這種情況下,半徑是 Rank 的值。 優化問題的解決方案將是圓接觸帕累托前沿的點。 原始問題的解決方案將是對應於不同圓半徑的點集,如下圖所示(不同等級的圓部分顯示為藍色虛線曲線):

這種方法可以更好地處理極值,但仍然存在兩個問題:首先——我希望在參考點附近有更多的點,以更好地克服線性組合所面臨的問題。 其次——CA 和圈複雜度本質上是不同的,並且具有不同的值集,因此我們需要對它們進行規範化(例如,使兩個指標的所有值都從 1 到 100)。

這是我們可以用來解決第一個問題的一個小技巧——我們可以查看它們的倒置值,而不是查看 CA 和 Cyclomatic Complexity。 在這種情況下,參考點將是 (0,0)。 為了解決第二個問題,我們可以使用最小值標準化指標。 這是它的外觀:

倒置和歸一化複雜度——NormComplexity:

(1 + min(複雜度)) / (1 + 複雜度)∗100

倒置和歸一化 CA – NormCA:

(1 + min(CA)) / (1+CA)∗100

注意:我加了 1 以確保沒有除以 0。

下圖顯示了一個反轉值的圖:

最終排名

我們現在來到最後一步——計算排名。 如前所述,我使用的是參考點方法,所以我們唯一需要做的就是計算向量的長度,對其進行規範化,並使其隨著為類創建單元測試的重要性而提升。 這是最終的公式:

Rank(NormComplexity , NormCA) = 100 - √(​​NormComplexity 2 + NormCA 2 ) / √2

更多統計數據

我還想補充一個想法,但讓我們先看一些統計數據。 這是耦合指標的直方圖:

這張圖有趣的是低 CA (0-2) 的類的數量。 CA 0 的類要么根本不使用,要么是頂級服務。 這些代表 API 端點,所以我們有很多它們很好。 但是具有 CA 1 的類是端點直接使用的類,我們擁有的這些類比端點多。 從架構/設計的角度來看,這意味著什麼?

一般來說,這意味著我們有一種面向腳本的方法——我們分別編寫每個業務案例(我們不能真正重用代碼,因為業務案例過於多樣化)。 如果是這樣,那肯定是代碼異味,我們需要進行重構。 否則,說明我們系統的內聚度低,這種情況我們也需要重構,但這次是架構重構。

我們可以從上面的直方圖中獲得的其他有用信息是,我們可以從符合單元測試覆蓋條件的類列表中完全過濾掉低耦合類({0,1} 中的 CA)。 但是,相同的類是集成/功能測試的良好候選者。

你可以在這個 GitHub 存儲庫中找到我使用過的所有腳本和資源:ashalitkin/code-base-stats。

它總是有效嗎?

不必要。 首先,這都是關於靜態分析,而不是運行時。 如果一個類與許多其他類相關聯,則可能表明它已被大量使用,但並非總是如此。 例如,我們不知道最終用戶是否真的大量使用該功能。 其次,如果系統的設計和質量足夠好,那麼它的不同部分/層很可能通過接口解耦,因此對 CA 的靜態分析不會給我們一個真實的畫面。 我想這是 CA 在 Sonar 等工具中不那麼受歡迎的主要原因之一。 幸運的是,這對我們來說完全沒問題,如果你還記得的話,我們有興趣將它專門應用於舊的醜陋代碼庫。

一般來說,我會說運行時分析會產生更好的結果,但不幸的是,它的成本更高、更耗時且更複雜,因此我們的方法是一種潛在有用且成本更低的替代方案。

相關:單一職責原則:偉大代碼的秘訣