動態

詳情 返回 返回

公司最大的內卷,偷偷做單元測試 - 動態 詳情

一位讀者在看過我的《理解這八大優勢,才算精通單元測試》後,問我:知道單元測試有好處,但實在沒空寫。看完文章後又想重新落實一下,有沒有啥寫好單元測試的技巧?

這位讀者絕對不是第一個和我抱怨單元測試的人。這很好理解,中國互聯網公司太多太卷,想要搶奪市場就要推出不同功能,而這些壓力一部分落在了程序員身上,拼命趕需求。單元測試這種費力不討好的事情,自然而然就沒有人做。

就我多年的經驗來看,寫單元測試其實不會拖延項目,反而能夠加快功能研發進度。單元測試的好處我就不在這裏贅述了,只有真正嘗試過的人才能理解。

馬克·吐温曾説:“取得成功的秘訣就是開始”。本篇文章想和大家分享一下寫好單元測試的技巧,希望可以給大家帶來新方向。

單元測試技巧-1

一、單元測試的注意事項

單元測試是為了讓我們快速查找並隔離損壞的代碼片段。正因如此,這些函數和類在測試時不應該依賴於mock(模擬)和stub(存根)以外的其他元素。在測試中,如果試圖覆蓋的邏輯過於複雜,就難以確保覆蓋的可靠性,也難以準確找出失敗的原因。

因此,我們要注意單元測試包括以下幾點。

01 簡潔性

短函數更容易閲讀和理解。我們每次只測試一個邏輯點,因此測試代碼應該控制在幾行之內。但如果是高級邏輯可能具有多個依賴項,這就需要大量樣板代碼來初始化模擬和存根。此外,單元測試同樣適用DRY原則(Don’t repeat yourself,一次且僅一次),我們在寫單元測試時要避免到處複製粘貼混亂的代碼,最好使用組合而不是繼承。

愛因斯坦曾説:“當你在生活中感到困頓時,也許是你把事情複雜化了。”所以,當我們對單元測試產生困惑時,也許是因為我們在單元測試中使用複雜的邏輯。注意一點:單元測試的目的在於測試代碼,不要讓單元測試本身也成為測試的一部分。

02 明確性

單元測試要使用詳盡的長名稱。這樣的名稱不僅能清楚表達信息,還能起到索引作用、快速定位相應測試。就算需求發生變化,我們只需要針對相應的測試進行更改,不必查看所有內容並檢查受影響的內容。

好的單元測試一般只有一個斷言,因此命名起來也很容易。例如,在處理金額計算時,it('should return 0 for an empty cart')要比it('works for 0') 或者 it('empty cart')好得多。對於使用函數名稱作為測試名稱的框架也是如此,shouldReturnZeroForAnEmptyCart就是一個很不錯的的命名。

正如丁玲所言:“人生就像爬坡,要一步一步來。”單元測試也是如此,不要一次性測試整個方法,要一步一步來。 我們只針對單個需求寫單元測試,代碼就會變得易於閲讀和維護。

03 可維護性

測試框架需要提供各種斷言方法。它們提供不同的方法來檢查結果,並且當斷言失敗時,它們還會顯示更具體的錯誤消息,從而提供更多上下文來查看錯誤所在。

例如,

expect(result === expected).toBeTruthy();

將會失敗

expect(received).toBeTruthy()
Received: false

儘管

expect(result).toBe(expected);

將提供更多有關具體失敗原因的信息:

expect(received).toBe(expected) // Object.is equality
Expected: "John Doe"
Received: "JohnDoe"

框架還為不同的測試方式提供了各種斷言。例如,在使用Jest進行測試時,toBe使用Object.is測試是否完全相等,而toEqualtoStrictEqual則深入比較對象,確保他們的類型和結構一致。

為了判斷浮點數是否相等,我們需要採用一種特殊的匹配器,這種匹配器能夠忽略由於浮點數在內存中的表示方式導致的微小舍入誤差。在Jest中,匹配器是toBeCloseTo。雖然toEqual有時也能適用,但即使是看似簡單的測試,如expect(0.1+0.2).toEqual(0.3)也可能無法通過。
單元測試技巧-2

二、單元測試的AAA原則

遵循AAA原則(Arrange、Act、Assert,安排、執行、斷言),可以嫺熟提升單元測試代碼的清晰度、可靠性和可維護性。

第一步,安排階段(Arrange)。 我們需要完成變量賦值、對象實例化對象以及測試運行所需的其餘前置設置,並且定義預期結果。這樣做的好處在於:一方面,我們需要在執行測試邏輯前就有明確預期;另一方面,這更方便在輸入數據後立即查看預期輸出,有助於避免代碼混淆。

第二步,執行階段(Act)。 我們將執行測試函數並存儲其結果。結果存儲其實是準備工作的自然延伸,有助於我們對結果進行回顧總結。

第三步,斷言階段(Assert)。 我們在這個階段可以判斷假設的正確性了。這正是單元測試的核心所在,因為這一環節實際上是對某些具體內容的測試。其目的在於是檢查實際得到的結果否與預期結果相匹配。

我們要確保代碼可靠性,避免錯誤輸入、缺少參數、空數據、調用函數中的異常等情況的出現。代碼覆蓋率工具可以幫助我們查漏補缺,找到未測試的代碼分支。我們要始終明確我們單元測試的目標,過於追求100%測試覆蓋率反而會讓單元測試代碼越來越繁雜。這與《呂氏春秋》中的論點不謀而合:“不知輕重,則重者為輕,輕者為重矣。若此,則每動無不敗”。

單元測試-3

三、單元測試的優化和維護

為了提高單元測試效率,我們需要模擬所有可能影響速度的外部依賴項,例如API調用、數據庫或文件系統訪問。我們在寫單元測試時,應儘量避免線程休眠、等待和超時。如果必須設置超時,就應該將其縮短至幾毫秒。在處理多線程或異步競爭條件時,精確控制出發條件比簡單的等待要有效得多。

單元測試應當確保不會改變作用域外的任何內容。 如果測試僅在按照特定順序執行時才能成功,這可能表明測試用例或測試代碼存在問題。每個測試用例應獨立運作。由於現代測試框架默認並行執行測試,因此我們不應依賴全局變量或之前測試的遺留效應。這也是全局變量常被視為不良編程習慣的原因之一,這會隱藏真正的依賴關係,導致代碼耦合度升高,並在處理多線程問題時需要格外留意。

當測試需要複雜的重複配置時,應利用框架提供的設置和清理功能。這些功能保障了在每個測試用例或整個測試套件開始前後,相關代碼能夠得到執行。這樣,無論是單獨運行測試還是作為測試套件的一部分,都能確保測試結果的確定性,執行順序不會對測試結果造成影響。

四、單元測試貴在堅持

《荀子·大略》:“夫盡小者大,積微成著,德至者色澤洽,行盡而聲問遠。”單元測試的作用只有經過長期積累才會變得顯著。 其實,寫單元測試更多的是對自己的代碼負責。有測試用例的代碼,別人更容易看懂,以後別人接手你的代碼時,也可能放心做改動。

根據上述方法開始行動,單元測試也不是什麼難事,畢竟“世上無難事,只怕有心人”。我發現關於單元測試有很多讀者感興趣,還有人曾問我單元測試到底該由測試進行還是開發進行。如果大家感興趣,我也可以寫一篇文章和大家簡單分享一下。

*參考文章:Andriy Obrizan,How to Write Good Unit Tests: 14 Tips

user avatar xiaodiandideyangrouchuan 頭像 manongtuwei 頭像 ourbmc 頭像
點贊 3 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.