用於積極測試體驗的 8 個自動化測試最佳實踐
已發表: 2022-03-11難怪許多開發人員將測試視為浪費時間和精力的必要之惡:測試可能很乏味、效率低下,而且過於復雜。
我的第一次測試體驗很糟糕。 我在一個對代碼覆蓋率有嚴格要求的團隊工作。 工作流程是:實現一個特性,調試它,然後編寫測試以確保完整的代碼覆蓋率。 該團隊沒有集成測試,只有具有大量手動初始化模擬的單元測試,並且大多數單元測試在使用庫執行自動映射時測試了瑣碎的手動映射。 每個測試都試圖斷言每個可用的屬性,因此每次更改都會破壞數十個測試。
我不喜歡使用測試,因為它們被認為是一種耗時的負擔。 然而,我沒有放棄。 每次小改動後的置信度測試和自動化檢查都激起了我的興趣。 我開始閱讀和練習,並了解到測試如果做得好,既有益又有趣。
在本文中,我分享了八個我希望從一開始就知道的自動化測試最佳實踐。
為什麼需要自動化測試策略
自動化測試通常著眼於未來,但是當您正確實施它時,您會立即受益。 使用可以幫助您更好地完成工作的工具可以節省時間並使您的工作更加愉快。
想像一下,您正在開發一個系統,該系統從公司的 ERP 中檢索採購訂單並將這些訂單發送給供應商。 您在 ERP 中有以前訂購的商品的價格,但當前價格可能不同。 您想控制是否以更低或更高的價格下訂單。 您存儲了用戶偏好,並且您正在編寫代碼來處理價格波動。
您將如何檢查代碼是否按預期工作? 你可能會:
- 在開發人員的 ERP 實例中創建一個虛擬訂單(假設您事先設置了它)。
- 運行您的應用程序。
- 選擇該訂單並開始下訂單過程。
- 從 ERP 的數據庫中收集數據。
- 從供應商的 API 請求當前價格。
- 覆蓋代碼中的價格以創建特定條件。
您在斷點處停下來,可以一步一步地查看一個場景會發生什麼,但是有很多可能的場景:
| 優先 | ERP價格 | 供應商價格 | 我們應該下訂單嗎? | |
|---|---|---|---|---|
| 允許更高的價格 | 允許更低的價格 | |||
| 錯誤的 | 錯誤的 | 10 | 10 | 真的 |
| (這裡會有另外三種偏好組合,但是價格是一樣的,所以結果是一樣的。) | ||||
| 真的 | 錯誤的 | 10 | 11 | 真的 |
| 真的 | 錯誤的 | 10 | 9 | 錯誤的 |
| 錯誤的 | 真的 | 10 | 11 | 錯誤的 |
| 錯誤的 | 真的 | 10 | 9 | 真的 |
| 真的 | 真的 | 10 | 11 | 真的 |
| 真的 | 真的 | 10 | 9 | 真的 |
如果出現錯誤,公司可能會賠錢,損害其聲譽,或兩者兼而有之。 您需要檢查多個場景並多次重複檢查循環。 手動這樣做會很乏味。 但是測試可以提供幫助!
測試讓您無需調用不穩定的 API 即可創建任何上下文。 它們消除了重複點擊舊的和緩慢的界面的需要,這些界面在傳統的 ERP 系統中太常見了。 您所要做的就是定義單元或子系統的上下文,然後任何調試、故障排除或場景探索都會立即發生——您運行測試並返回您的代碼。 我的偏好是在我的 IDE 中設置一個鍵綁定,以重複我之前的測試運行,在我進行更改時提供即時、自動的反饋。
1.保持正確的態度
與手動調試和自測相比,自動化測試從一開始就更有效率,甚至在提交任何測試代碼之前。 在檢查代碼的行為是否符合預期之後——通過手動測試,或者對於更複雜的模塊,在測試期間使用調試器逐步完成它——你可以使用斷言來定義你對任何輸入參數組合的期望。
測試通過後,您幾乎可以提交了,但還沒有完全準備好。 準備重構您的代碼,因為第一個工作版本通常並不優雅。 你會在沒有測試的情況下執行重構嗎? 這是有問題的,因為您必須再次完成所有手動步驟,這可能會降低您的熱情。
未來呢? 在執行任何重構、優化或功能添加時,測試有助於確保模塊在您更改後仍能按預期運行,從而灌輸持久的信心,讓開發人員感到更有能力應對即將到來的工作。
將測試視為負擔或僅使代碼審查員或領導感到高興的事情會適得其反。 測試是我們作為開發人員從中受益的工具。 我們喜歡我們的代碼正常工作,我們不喜歡花時間在重複的操作或修復代碼以解決錯誤上。
最近,我致力於重構我的代碼庫,並要求我的 IDE 清理未使用的using指令。 令我驚訝的是,測試顯示我的電子郵件報告系統出現了幾次故障。 然而,這是一個有效的失敗——清理過程在我的 Razor (HTML + C#) 代碼中刪除了一些用於電子郵件模板的using指令,因此模板引擎無法構建有效的 HTML。 我沒想到這麼小的操作會破壞電子郵件報告。 測試幫助我避免了在應用發布前花費數小時在應用程序上發現錯誤,當時我認為一切都會正常工作。
當然,你必須知道如何使用工具,而不是像諺語所說的那樣割傷你的手指。 定義上下文似乎很乏味,並且比運行應用程序更難,測試需要太多維護以避免變得陳舊和無用。 這些都是有效的觀點,我們將解決它們。
2. 選擇正確的測試類型
開發人員經常變得不喜歡自動化測試,因為他們試圖模擬十幾個依賴項只是為了檢查它們是否被代碼調用。 或者,開發人員遇到高級測試並嘗試重現每個應用程序狀態以檢查小模塊中的所有變化。 這些模式既無效率又乏味,但我們可以通過使用不同的測試類型來避免它們。 (畢竟,測試應該是實用和愉快的!)
讀者需要知道什麼是單元測試以及如何編寫它們,並熟悉集成測試——如果沒有,這裡值得停下來快速了解一下。
有幾十種測試類型,但是這五種常見的類型是非常有效的組合:
- 單元測試用於通過直接調用其方法來測試隔離模塊。 依賴項沒有被測試,因此,它們被嘲笑。
- 集成測試用於測試子系統。 您仍然使用對模塊自己的方法的直接調用,但這裡我們關心依賴關係,所以不要使用模擬依賴關係——只有真正的(生產)依賴模塊。 您仍然可以使用內存數據庫或模擬 Web 服務器,因為它們是基礎設施的模擬。
- 功能測試是針對整個應用程序的測試,也稱為端到端 (E2E) 測試。 您不使用直接呼叫。 相反,所有交互都通過 API 或用戶界面進行——這些是從最終用戶角度進行的測試。 但是,基礎設施仍然受到嘲笑。
- Canary 測試類似於功能測試,但具有生產基礎設施和一組較小的操作。 它們用於確保新部署的應用程序正常工作。
- 負載測試類似於金絲雀測試,但具有真實的登台基礎設施和更小的操作集,這些操作會重複很多次。
並不總是需要從一開始就使用所有五種測試類型。 在大多數情況下,您可以通過前三個測試走很長的路。
我們將簡要檢查每種類型的用例,以幫助您選擇適合您需求的用例。
單元測試
回想一下具有不同價格和處理偏好的示例。 它是單元測試的一個很好的候選者,因為我們只關心模塊內部發生的事情,並且結果具有重要的業務影響。
該模塊有很多不同的輸入參數組合,我們希望為每個有效參數組合獲得一個有效的返回值。 單元測試擅長確保有效性,因為它們提供了對函數或方法的輸入參數的直接訪問,並且您不必編寫數十種測試方法來涵蓋每種組合。 在許多語言中,您可以通過定義一個方法來避免重複測試方法,該方法接受您的代碼和預期結果所需的參數。 然後,您可以使用您的測試工具為該參數化方法提供不同的值集和期望值。
集成測試
當您對模塊如何與其依賴項、其他模塊或基礎設施交互感興趣時,集成測試非常適合。 您仍然使用直接方法調用,但無法訪問子模塊,因此嘗試測試所有子模塊的所有輸入法的所有場景是不切實際的。
通常,我更喜歡每個模塊有一個成功場景和一個失敗場景。
我喜歡使用集成測試來檢查依賴注入容器是否構建成功,處理或計算管道是否返回預期結果,或者是否從數據庫或第三方 API 正確讀取和轉換複雜數據。
功能或 E2E 測試
這些測試讓您對您的應用程序正常工作充滿信心,因為它們驗證您的應用程序至少可以在沒有運行時錯誤的情況下啟動。 在不直接訪問其類的情況下開始測試您的代碼需要做更多的工作,但是一旦您理解並編寫了前幾個測試,您會發現這並不太難。
如果需要,通過使用命令行參數啟動進程來運行應用程序,然後像潛在客戶一樣使用應用程序:調用 API 端點或按下按鈕。 這並不難,即使是在 UI 測試的情況下:每個主要平台都有一個工具可以在 UI 中找到視覺元素。
金絲雀測試
功能測試讓您知道您的應用程序是否在測試環境中工作,但生產環境呢? 假設您正在使用多個第三方 API,並且您想要擁有它們狀態的儀表板,或者想要查看您的應用程序如何處理傳入的請求。 這些是金絲雀測試的常見用例。
它們通過簡單地作用於工作系統來運行,而不會對第三方系統造成副作用。 例如,您無需下訂單即可註冊新用戶或檢查產品可用性。
金絲雀測試的目的是確保所有主要組件在生產環境中協同工作,不會因為憑證問題而失敗。
負載測試
負載測試揭示了當大量人開始使用您的應用程序時,它是否會繼續工作。 它們類似於金絲雀和功能測試,但不在本地或生產環境中進行。 通常會使用特殊的暫存環境,類似於生產環境。
需要注意的是,這些測試不使用真正的第三方服務,這可能會對其生產服務的外部負載測試不滿意,因此可能會收取額外費用。
3. 保持測試類型分開
在設計自動化測試計劃時,應將每種類型的測試分開,以便能夠獨立運行。 雖然這需要額外的組織,但這是值得的,因為混合測試會產生問題。
這些測試有不同:
- 意圖和基本概念(因此將它們分開為下一個查看代碼的人提供了一個很好的先例,包括“未來的你”)。
- 執行時間(因此首先運行單元測試可以在測試失敗時加快測試週期)。
- 依賴項(因此僅加載測試類型中需要的那些會更有效)。
- 所需的基礎設施。
- 編程語言(在某些情況下)。
- 在持續集成 (CI) 管道中或之外的位置。
重要的是要注意,對於大多數語言和技術堆棧,您可以將所有單元測試與以功能模塊命名的子文件夾組合在一起。 這很方便,減少了創建新功能模塊時的摩擦,更容易自動構建,減少混亂,並且是簡化測試的另一種方法。
4. 自動運行你的測試
想像一下這樣一種情況,您已經編寫了一些測試,但在幾週後提取您的 repo 後,您注意到這些測試不再通過。
這是一個令人不快的提醒,即測試是代碼,並且與任何其他代碼一樣,它們需要維護。 最好的時間是在您認為自己已經完成工作並想看看一切是否仍按預期運行之前。 您擁有所需的所有上下文,並且可以比在不同子系統上工作的同事更輕鬆地修復代碼或更改失敗的測試。 但是這一刻只存在於你的腦海中,所以最常見的運行測試的方式是在推送到開發分支或創建拉取請求之後自動運行。

這樣,您的主分支將始終處於有效狀態,或者您至少可以清楚地指示其狀態。 自動化構建和測試管道(或 CI 管道)有助於:
- 確保代碼是可構建的。
- 消除潛在的“它適用於我的機器”問題。
- 提供有關如何準備開發環境的可運行說明。
配置此管道需要時間,但即使您是唯一的開發人員,該管道也可以在用戶或客戶面前發現一系列問題。
一旦運行,CI 還會在新問題有機會擴大範圍之前揭示它們。 因此,我更喜歡在編寫第一個測試後立即設置它。 您可以將代碼託管在 GitHub 上的私有存儲庫中並設置 GitHub Actions。 如果你的 repo 是公開的,你有比 GitHub Actions 更多的選擇。 例如,我的自動化測試計劃在 AppVeyor 上運行,用於具有數據庫和三種類型測試的項目。
我更喜歡為生產項目構建我的管道,如下所示:
- 編譯或轉譯
- 單元測試:它們很快並且不需要依賴項
- 數據庫或其他服務的設置和初始化
- 集成測試:它們在您的代碼之外具有依賴關係,但它們比功能測試更快
- 功能測試:當其他步驟成功完成後,運行整個應用程序
沒有金絲雀測試或負載測試。 由於它們的具體情況和要求,它們應該手動啟動。
5. 只寫必要的測試
為所有代碼編寫單元測試是一種常見的策略,但有時這會浪費時間和精力,並且不會給您任何信心。 如果您熟悉“測試金字塔”的概念,您可能會認為您的所有代碼都必須被單元測試覆蓋,只有一部分被其他更高級別的測試覆蓋。
我認為沒有必要編寫單元測試來確保以所需的順序調用多個模擬依賴項。 這樣做需要設置幾個模擬並驗證所有調用,但這仍然不能讓我確信模塊正在工作。 通常,我只編寫一個使用真正依賴項並只檢查結果的集成測試; 這讓我確信測試模塊中的管道工作正常。
一般來說,我編寫的測試可以讓我的生活更輕鬆,同時實現功能並在以後支持它。
對於大多數應用程序而言,以 100% 的代碼覆蓋率為目標會增加大量繁瑣的工作,並且通常會消除使用測試和編程的樂趣。 正如 Martin Fowler 的測試覆蓋率所說:
測試覆蓋率是查找代碼庫中未測試部分的有用工具。 測試覆蓋率作為測試有多好的數字聲明幾乎沒有用。
因此,我建議您在編寫一些測試後安裝並運行覆蓋率分析器。 帶有突出顯示的代碼行的報告將幫助您更好地了解其執行路徑並找到應覆蓋的未發現位置。 此外,查看您的 getter、setter 和外觀,您會明白為什麼 100% 的覆蓋率並不有趣。
6. 玩樂高
有時,我會看到諸如“如何測試私有方法?”之類的問題。 你沒有。 如果你問過這個問題,那麼事情已經出了問題。 通常,這意味著您違反了單一職責原則,並且您的模塊沒有正確執行某些操作。
重構這個模塊並將你認為重要的邏輯拉到一個單獨的模塊中。 增加文件數量沒有問題,這將導致代碼結構為樂高積木:非常易讀、可維護、可替換和可測試。
正確構建代碼說起來容易做起來難。 這裡有兩個建議:
函數式編程
函數式編程的原理和思想值得學習。 大多數主流語言,如 C、C++、C#、Java、Assembly、JavaScript 和 Python,都強制你為機器編寫程序。 函數式編程更適合人腦。
起初這似乎違反直覺,但請考慮一下:如果您將所有代碼放在一個方法中,使用共享內存塊存儲臨時值,並使用相當數量的跳轉指令,則計算機會很好。 此外,優化階段的編譯器有時會這樣做。 然而,人腦並不容易處理這種方法。
函數式編程迫使您以富有表現力的方式編寫沒有副作用、具有強類型的純函數。 這樣就更容易推理一個函數,因為它產生的唯一東西就是它的返回值。 Programming Throwdown 播客插曲與 Adam Gordon Bell 的函數式編程將幫助您獲得基本的理解,您可以繼續與 Philip Wadler 合作的 Corecursive 插曲 God's Programming Language 和 Bartosz Milewski 的類別理論。 最後兩個大大豐富了我對編程的認知。
測試驅動開發
我建議掌握 TDD。 最好的學習方法是練習。 字符串計算器 Kata 是練習代碼 kata 的好方法。 掌握 kata 需要時間,但最終會讓您完全吸收 TDD 的思想,這將幫助您創建結構良好的代碼,使用起來很愉快並且可測試。
需要注意的一點:有時您會看到 TDD 純粹主義者聲稱 TDD 是唯一正確的編程方式。 在我看來,它只是您工具箱中的另一個有用工具,僅此而已。
有時,您需要了解如何調整模塊和流程之間的關係,但不知道要使用哪些數據和簽名。 在這種情況下,編寫代碼直到它編譯,然後編寫測試來解決和調試功能。
在其他情況下,您知道所需的輸入和輸出,但由於復雜的邏輯,不知道如何正確編寫實現。 對於這些情況,開始遵循 TDD 過程並逐步構建代碼比花時間考慮完美的實現更容易。
7. 保持測試簡單而專注
很高興在一個組織整齊的代碼環境中工作,沒有不必要的干擾。 這就是為什麼將 SOLID、KISS 和 DRY 原則應用於測試很重要——在需要時使用重構。
有時我會聽到這樣的評論,“我討厭在經過大量測試的代碼庫中工作,因為每次更改都需要我修復數十個測試。” 這是由於測試不集中並試圖測試太多而導致的高維護問題。 “做好一件事”的原則也適用於測試:“做好一件事”; 每個測試應該相對較短,只測試一個概念。 “很好地測試一件事”並不意味著每次測試只能使用一個斷言:如果要測試非平凡且重要的數據映射,則可以使用數十個斷言。
這個重點不限於一個特定的測試或測試類型。 想像一下處理您使用單元測試測試的複雜邏輯,例如將數據從 ERP 系統映射到您的結構,並且您有一個訪問模擬 ERP API 並返回結果的集成測試。 在這種情況下,記住單元測試已經涵蓋的內容很重要,這樣您就不會在集成測試中再次測試映射。 通常,確保結果具有正確的標識字段就足夠了。
使用像樂高積木和集中測試這樣的代碼結構,對業務邏輯的更改應該不會很痛苦。 如果更改是激進的,您只需刪除文件及其相關測試,並使用新測試進行新實現。 如果發生較小的更改,您通常會更改一到三個測試以滿足新要求並更改邏輯。 更改測試很好; 您可以將這種做法視為複式簿記。
其他實現簡單的方法包括:
- 提出測試文件結構、測試內容結構(通常是 Arrange-Act-Assert 結構)和測試命名的約定; 然後,最重要的是,始終如一地遵循這些規則。
- 將大代碼塊提取到“準備請求”等方法,並為重複操作製作輔助函數。
- 將構建器模式應用於測試數據配置。
- 使用(在集成測試中)您在主應用程序中使用的相同 DI 容器,因此每次實例化都將與
TestServices.Get()一樣簡單,無需手動創建依賴項。 這樣就可以很容易地閱讀、維護和編寫新的測試,因為你已經有了有用的助手。
如果您覺得測試變得過於復雜,只需停下來想一想。 模塊或您的測試都需要重構。
8.使用工具讓你的生活更輕鬆
測試時您將面臨許多繁瑣的任務。 例如,設置測試環境或數據對象,為依賴項配置存根和模擬,等等。 幸運的是,每個成熟的技術堆棧都包含多種工具,可以讓這些任務變得不那麼乏味。
如果你還沒有寫過你的前一百個測試,我建議你先寫一些測試,然後花一些時間來識別重複性任務,並為你的技術棧學習與測試相關的工具。
為了獲得靈感,您可以使用以下工具:
- 測試跑步者。 尋找簡潔的語法和易用性。 根據我的經驗,對於 .NET,我推薦 xUnit(儘管 NUnit 也是一個不錯的選擇)。 對於 JavaScript 或 TypeScript,我選擇 Jest。 嘗試找到最適合您的任務和思維方式的方式,因為工具和挑戰會不斷發展。
- 模擬庫。 可能有代碼依賴項的低級模擬,如接口,但也有 Web API 或數據庫的高級模擬。 對於 JavaScript 和 TypeScript,Jest 中包含的低級模擬是可以的。 對於.NET。 我使用最小起訂量,雖然 NSubstitute 也很棒。 至於 Web API 模擬,我喜歡使用 WireMock.NET。 它可以代替 API 用於對響應處理進行故障排除和調試。 它在自動化測試中也非常可靠和快速。 可以使用內存中的對應物來模擬數據庫。 .NET 中的 EfCore 提供了這樣一個選項。
- 數據生成庫。 這些實用程序用隨機數據填充您的數據對象。 例如,當您只關心來自大數據傳輸對象的幾個字段(如果那樣的話;也許您只想測試映射的正確性)時,它們很有用。 您可以將它們用於測試,也可以作為隨機數據顯示在表單上或填充數據庫。 出於測試目的,我在 .NET 中使用 AutoFixture。
- UI 自動化庫。 這些是用於自動化測試的自動化用戶:他們可以運行您的應用程序、填寫表單、單擊按鈕、閱讀標籤等。 要瀏覽應用程序的所有元素,您無需處理通過坐標單擊或圖像識別; 主要平台具有按類型、標識符或數據查找所需元素的工具,因此您無需在每次重新設計時更改測試。 它們很健壯,所以一旦你讓它們為你和 CI 工作(有時你發現事情只在你的機器上工作),它們就會繼續工作。 我喜歡將 FlaUI 用於 .NET,將 Cypress 用於 JavaScript 和 TypeScript。
- 斷言庫。 大多數測試運行程序都包含斷言工具,但在某些情況下,獨立工具可以幫助您使用更清晰、更易讀的語法編寫複雜的斷言,例如 Fluent Assertions for .NET。 我特別喜歡斷言集合相等的函數,而不管項目的順序或其在內存中的地址如何。
願流與你同在
幸福與《心流:最佳體驗的心理學》一書中詳細描述的所謂“心流”體驗緊密相連。 要獲得這種心流體驗,您必須從事具有明確目標的活動,並且能夠看到自己的進步。 任務應該產生即時反饋,自動化測試是理想的選擇。 您還需要在挑戰和技能之間取得平衡,這取決於每個人。 測試,尤其是在使用 TDD 時,可以幫助指導您並灌輸信心。 它們幫助您設定具體目標,每個通過的測試都是您進步的指標。
正確的測試方法可以讓你更快樂、更有效率,並且測試可以減少倦怠的機會。 關鍵是將測試視為一種工具(或工具集),它可以幫助您進行日常開發,而不是讓您的代碼適應未來的繁重步驟。
測試是編程的必要組成部分,它允許軟件工程師改進他們的工作方式,提供最佳結果,並最佳地利用他們的時間。 也許更重要的是,測試可以幫助開發人員更享受他們的工作,從而提高他們的士氣和動力。
