不穩定的測試:擺脫測試中的噩夢

已發表: 2022-03-10
快速總結↬不可靠的測試對於任何編寫自動化測試或關注結果的人來說都是一場噩夢。 不穩定的測試甚至給人們帶來了噩夢和不眠之夜。 在本文中,Ramona Schwering 分享了她的經驗,以幫助您擺脫或避免陷入困境。

有一個寓言,這些天我想了很多。 這個寓言是小時候告訴我的。 它被伊索稱為“狼來了的男孩”。 這是關於一個男孩照顧他村里的羊的故事。 他感到無聊,假裝一隻狼正在攻擊羊群,向村民呼救——結果他們失望地意識到這是一場虛驚,讓男孩獨自一人。 然後,當狼真的出現,男孩呼救時,村民們認為這是又一次虛驚,沒有前來救援,結果羊被狼吃掉了。

作者自己對這個故事的寓意進行了最好的總結:

“即使他說的是真話,也不會相信一個騙子。”

一隻狼襲擊了羊,男孩哭著求救,但在無數次謊言之後,沒有人相信他了。 這種道德可以應用於測試:Aesop 的故事是我偶然發現的匹配模式的一個很好的寓言:無法提供任何價值的片狀測試。

前端測試:為什麼還要麻煩?

我大部分時間都花在前端測試上。 因此,本文中的代碼示例將主要來自我在工作中遇到的前端測試,這不足為奇。 但是,在大多數情況下,它們可以很容易地翻譯成其他語言並應用於其他框架。 所以,我希望這篇文章對你有用——不管你有什麼專業知識。

值得回顧一下前端測試的含義。 本質上,前端測試是一組用於測試 Web 應用程序 UI 的實踐,包括其功能。

作為一名質量保證工程師,我知道在發布前從清單中進行無休止的手動測試的痛苦。 因此,除了確保應用程序在連續更新期間保持無錯誤的目標之外,我還努力減輕由您實際上不需要人工執行的常規任務引起的測試工作量。 現在,作為一名開發人員,我發現這個話題仍然很重要,尤其是當我嘗試直接幫助用戶和同事時。 特別是測試有一個問題讓我們做噩夢。

片狀測試的科學

不穩定的測試是每次運行相同的分析時都無法產生相同結果的測試。 構建只會偶爾失敗:一次通過,另一次失敗,下一次再次通過,沒有對構建進行任何更改。

當我回想起我的測試噩夢時,我特別想到了一個案例。 它在 UI 測試中。 我們構建了一個自定義樣式的組合框(即帶有輸入字段的可選列表):

自定義選擇器示例
我每天工作的項目中的自定義選擇器。 (大預覽)

使用此組合框,您可以搜索產品並選擇一個或多個結果。 很多天,這個測試都​​很順利,但在某些時候,情況發生了變化。 在我們的持續集成 (CI) 系統中大約十個構建中的一個中,在此組合框中搜索和選擇產品的測試失敗了。

失敗的屏幕截圖顯示了未過濾的結果列表,儘管搜索已成功:

帶有不穩定測試的 CI 執行的屏幕截圖
片狀測試在行動:為什麼它只是有時而不是總是失敗? (大預覽)

像這樣的不穩定測試可能會阻塞持續部署管道,使功能交付比需要的慢。 此外,一個不穩定的測試是有問題的,因為它不再是確定性的——使它毫無用處。 畢竟,你不會信任一個騙子,就像你不會信任一個騙子一樣。

此外,易碎測試的修復成本很高,通常需要數小時甚至數天的時間來調試。 儘管端到端測試更容易出現問題,但我在各種測試中都經歷過它們:單元測試、功能測試、端到端測試以及介於兩者之間的所有測試。

易碎測試的另一個重要問題是它們灌輸給我們開發人員的態度。 當我開始從事測試自動化工作時,我經常聽到開發人員對失敗的測試這樣說:

“啊,那個建築。 沒關係,重新開始吧。 它最終會在某個時候過去。”

對我來說是一個巨大的危險信號。 它告訴我構建中的錯誤不會被認真對待。 假設一個不穩定的測試不是真正的錯誤,而是“只是”不穩定的,不需要照顧甚至調試。 反正以後考試會再通過的,對吧? 沒有! 如果合併了這樣的提交,在最壞的情況下,我們將在產品中進行新的不穩定測試。

跳躍後更多! 繼續往下看↓

原因

因此,片狀測試是有問題的。 我們應該怎麼處理它們? 好吧,如果我們知道問題所在,我們可以設計一個應對策略。

我在日常生活中經常遇到原因。 它們可以在測試本身中找到。 測試可能寫得不夠理想、假設錯誤或包含不良做法。 然而,不僅如此。 片狀測試可能表明情況更糟。

在以下部分中,我們將介紹我遇到的最常見的部分。

1. 測試端原因

在理想情況下,應用程序的初始狀態應該是原始的並且 100% 可預測。 實際上,您永遠不知道您在測試中使用的 ID 是否始終相同。

讓我們來看看我個人失敗的兩個例子。 第一個錯誤是在我的測試裝置中使用了一個 ID

 { "id": "f1d2554b0ce847cd82f3ac9bd1c0dfca", "name": "Variant product", }

第二個錯誤是在 UI 測試中尋找一個唯一的選擇器,然後想,“好吧,這個 ID 似乎是唯一的。 我會用的。”

 <!-- This is a text field I took from a project I worked on --> <input type="text" />

但是,如果我在另一個安裝上運行測試,或者稍後在 CI 中的多個構建上運行測試,那麼這些測試可能會失敗。 我們的應用程序將重新生成 ID,並在構建之間更改它們。 因此,第一個可能的原因是在硬編碼的 ID中找到。

第二個原因可能來自隨機(或以其他方式)生成的演示數據。 當然,你可能會認為這個“缺陷”是有道理的——畢竟,數據生成是隨機的——但考慮調試這些數據。 很難看出錯誤是在測試本身還是在演示數據中。

接下來是我無數次掙扎的測試方面的原因:具有交叉依賴關係的測試。 有些測試可能無法獨立運行或以隨機順序運行,這是有問題的。 此外,之前的測試可能會干擾後續測試。 這些場景可能會通過引入副作用而導致不穩定的測試。

但是,不要忘記測試是關於具有挑戰性的假設。 如果您的假設一開始就有缺陷,會發生什麼? 我經常經歷這些,我最喜歡的是關於時間的有缺陷的假設。

一個例子是使用不准確的等待時間,尤其是在 UI 測試中——例如,通過使用固定等待時間。 以下行來自 Nightwatch.js 測試。

 // Please never do that unless you have a very good reason! // Waits for 1 second browser.pause(1000);

另一個錯誤的假設與時間本身有關。 我曾經發現一個不穩定的 PHPUnit 測試只在我們的夜間構建中失敗。 經過一番調試,我發現昨天和今天之間的時間偏移是罪魁禍首。 另一個很好的例子是時區導致的失敗。

錯誤的假設不止於此。 我們也可能對數據的順序有錯誤的假設。 想像一個包含多個信息條目的網格或列表,例如貨幣列表:

我們項目中使用的自定義列表組件
我們項目中使用的自定義列表組件。 (大預覽)

我們希望使用第一個條目的信息,即“捷克克朗”貨幣。 您能否確定每次執行測試時您的應用程序總是將這條數據作為第一個條目? 會不會是“歐元”或其他貨幣在某些情況下會成為第一個入口?

不要假設您的數據會按照您需要的順序出現。 與硬編碼的 ID 類似,構建之間的順序可以更改,具體取決於應用程序的設計。

2. 環境方面的原因

下一類原因與測試之外的一切有關。 具體來說,我們談論的是執行測試的環境,測試之外的 CI 和 docker 相關的依賴項——所有這些你幾乎無法影響的事情,至少在你作為測試人員的角色中是這樣。

一個常見的環境方面的原因是資源洩漏:通常這將是負載下的應用程序,導致不同的加載時間或意外行為。 大型測試很容易導致洩漏,佔用大量內存。 另一個常見問題是缺乏清理

依賴項之間的不兼容尤其讓我做噩夢。 當我使用 Nightwatch.js 進行 UI 測試時,發生了一場噩夢。 Nightwatch.js 使用 WebDriver,這當然依賴於 Chrome。 當 Chrome 進行更新時,兼容性出現了問題:Chrome、WebDriver 和 Nightwatch.js 本身不再協同工作,這導致我們的構建時常失敗。

說到依賴關係值得一提的是任何 npm 問題,例如缺少權限或 npm 關閉。 我在觀察 CI 時經歷了所有這些。

當涉及到由於環境問題導致的 UI 測試錯誤時,請記住,您需要整個應用程序堆棧才能運行它們。 涉及的東西越多,出錯的可能性就越大。 因此,JavaScript 測試是 Web 開發中最難穩定的測試,因為它們涵蓋了大量代碼。

3. 產品方面的原因

最後但並非最不重要的一點是,我們真的必須小心第三個區域——一個存在實際錯誤的區域。 我說的是產品方面的片狀原因。 最著名的例子之一是應用程序中的競爭條件。 發生這種情況時,需要在產品中修復錯誤,而不是在測試中! 在這種情況下,嘗試修復測試或環境將毫無用處。

對抗片狀的方法

我們已經確定了三個導致片狀的原因。 我們可以在此基礎上製定我們的反制策略! 當然,當您遇到不穩定的測試時,牢記這三個原因,您已經獲得了很多。 您已經知道要尋找什麼以及如何改進測試。 但是,除此之外,還有一些策略可以幫助我們設計、編寫和調試測試,我們將在下面的部分中一起研究它們。

專注於你的團隊

你的團隊可以說是最重要的因素。 作為第一步,承認你有片狀測試的問題。 獲得整個團隊的承諾至關重要! 然後,作為一個團隊,您需要決定如何處理不穩定的測試。

在我從事技術工作的這些年裡,我遇到了團隊用來應對脆弱性的四種策略:

  1. 什麼都不做,接受不穩定的測試結果。
    當然,這種策略根本不是解決方案。 測試不會產生任何價值,因為你不再信任它——即使你接受它的脆弱性。 所以我們可以很快跳過這個。
  2. 重試測試,直到通過。
    這種策略在我職業生涯初期很常見,導致了我之前提到的反應。 重試測試直到他們通過之前有一些接受。 這種策略不需要調試,但它很懶惰。 除了隱藏問題的症狀外,它還會進一步降低您的測試套件的速度,從而使解決方案不可行。 但是,這條規則可能有一些例外,我稍後會解釋。
  3. 刪除並忘記測試。
    這是不言自明的:只需刪除不穩定的測試,這樣它就不會再乾擾您的測試套件了。 當然,它會為您省錢,因為您不再需要調試和修復測試。 但這是以失去一些測試覆蓋率和失去潛在的錯誤修復為代價的。 測試的存在是有原因的! 不要通過刪除測試來射擊信使。
  4. 隔離和修復。
    我在這個策略上取得了最大的成功。 在這種情況下,我們會暫時跳過測試,並讓測試套件不斷提醒我們已經跳過了一個測試。 為了確保修復不會被忽視,我們會為下一個 sprint 安排一張票。 機器人提醒也很有效。 一旦導致片狀的問題得到解決,我們將再次集成(即取消跳過)測試。 不幸的是,我們將暫時失去覆蓋範圍,但它會回來修復,所以這不會花很長時間。
跳過的測試,取自我們 CI 的報告
跳過的測試,取自我們 CI 的報告。 (大預覽)

這些策略幫助我們處理工作流級別的測試問題,而且我不是唯一遇到這些問題的人。 Sam Saffron 在他的文章中得出了類似的結論。 但在我們的日常工作中,它們對我們的幫助有限。 那麼,當這樣的任務出現時,我們該如何進行呢?

保持測試隔離

在規劃您的測試用例和結構時,請始終將您的測試與其他測試隔離,以便它們能夠以獨立或隨機的順序運行。 最重要的一步是在測試之間恢復乾淨的安裝。 此外,僅測試您要測試的工作流,並僅為測試本身創建模擬數據。 這個快捷方式的另一個優點是它可以提高測試性能。 如果您遵循這些要點,其他測試或剩餘數據的副作用將不會妨礙您。

下面的例子取自一個電商平台的UI測試,處理的是客戶在店鋪店面的登錄。 (測試是用 JavaScript 編寫的,使用 Cypress 框架。)

 // File: customer-login.spec.js let customer = {}; beforeEach(() => { // Set application to clean state cy.setInitialState() .then(() => { // Create test data for the test specifically return cy.setFixture('customer'); }) }):

第一步是將應用程序重置為全新安裝。 這是beforeEach生命週期鉤子中的第一步,以確保每次都執行重置。 之後,專門為測試創建測試數據——對於這個測試用例,將通過自定義命令創建一個客戶。 隨後,我們可以從我們要測試的一個工作流程開始:客戶的登錄。

進一步優化測試結構

我們可以做一些其他的小調整,使我們的測試結構更加穩定。 第一個很簡單:從較小的測試開始。 如前所述,你在測試中做的越多,出錯的可能性就越大。 保持測試盡可能簡單,並避免在每個測試中使用大量邏輯。

當涉及到不假設數據的順序時(例如,在 UI 測試中處理列表中條目的順序時),我們可以設計一個獨立於任何順序的測試。 為了帶回包含信息的網格示例,我們不會使用偽選擇器或其他對順序有很強依賴性的 CSS。 代替nth-child(3)選擇器,我們可以使用文本或其他順序無關緊要的東西。 例如,我們可以使用一個斷言,如“在此表中查找包含此文本字符串的元素”。

等待! 測試重試有時可以嗎?

重試測試是一個有爭議的話題,這是理所當然的。 如果盲目地重試測試直到成功,我仍然認為它是一種反模式。 但是,有一個重要的例外:當您無法控制錯誤時,重試可能是最後的手段(例如,從外部依賴項中排除錯誤)。 在這種情況下,我們無法影響錯誤的來源。 但是,在執行此操作時要格外小心:重試測試時不要對脆弱視而不見,並在跳過測試時使用通知來提醒您。

以下示例是我在我們的 CI 中與 GitLab 一起使用的示例。 其他環境可能有不同的語法來實現重試,但這應該讓您體驗一下:

 test: script: rspec retry: max: 2 when: runner_system_failure

在這個例子中,我們正在配置如果作業失敗應該重試多少次。 有趣的是,如果 runner 系統出現錯誤(例如,作業設置失敗),重試的可能性。 僅當 docker 設置中的某些內容失敗時,我們才選擇重試我們的工作。

請注意,這將在觸發時重試整個作業。 如果您只想重試錯誤的測試,那麼您需要在測試框架中尋找一個功能來支持這一點。 下面是 Cypress 的一個示例,它從版本 5 開始支持重試單個測試:

 { "retries": { // Configure retry attempts for 'cypress run` "runMode": 2, // Configure retry attempts for 'cypress open` "openMode": 2, } }

您可以在賽普拉斯的配置文件cypress.json中激活測試重試。 在那裡,您可以在測試運行器和無頭模式下定義重試嘗試。

使用動態等待時間

這一點對於各種測試都很重要,尤其是 UI 測試。 我怎麼強調都不為過:永遠不要使用固定的等待時間——至少不要沒有很好的理由。 如果您這樣做,請考慮可能的結果。 在最好的情況下,您會選擇太長的等待時間,從而使測試套件比需要的慢。 在最壞的情況下,您不會等待足夠長的時間,因此測試將不會繼續,因為應用程序還沒有準備好,導致測試以不穩定的方式失敗。 根據我的經驗,這是導致片狀測試的最常見原因。

相反,使用動態等待時間。 有很多方法可以做到這一點,但賽普拉斯處理得特別好。

所有賽普拉斯命令都有一個隱式等待方法:它們已經檢查了應用命令的元素是否在指定時間內存在於 DOM 中——指向賽普拉斯的重試能力。 但是,它只檢查存在,僅此而已。 所以我建議更進一步——等待真實用戶也會看到的網站或應用程序 UI 中的任何更改,例如 UI 本身或動畫中的更改。

一個固定的等待時間,在賽普拉斯的測試日誌中找到
在賽普拉斯的測試日誌中找到一個固定的等待時間。 (大預覽)

此示例使用選擇器.offcanvas對元素使用顯式等待時間。 僅當元素在指定的超時之前可見時測試才會繼續,您可以配置:

 // Wait for changes in UI (until element is visible) cy.get(#element).should('be.visible');

賽普拉斯動態等待的另一個巧妙可能性是它的網絡功能。 是的,我們可以等待請求發生並等待其響應的結果。 我特別經常使用這種等待。 在下面的示例中,我們定義了要等待的請求,使用wait命令等待響應,並斷言其狀態碼:

 // File: checkout-info.spec.js // Define request to wait for cy.intercept({ url: '/widgets/customer/info', method: 'GET' }).as('checkoutAvailable'); // Imagine other test steps here... // Assert the response's status code of the request cy.wait('@checkoutAvailable').its('response.statusCode') .should('equal', 200);

通過這種方式,我們可以完全按照應用程序的需要等待,從而使測試更加穩定,並且由於資源洩漏或其他環境問題而不太容易出現片狀問題。

調試易碎測試

我們現在知道如何通過設計來防止片狀測試。 但是如果你已經在處理一個不穩定的測試怎麼辦? 你怎麼能擺脫它?

當我調試時,將有缺陷的測試放在一個循環中幫助我發現了片狀問題。 例如,如果您運行一個測試 50 次,並且每次都通過,那麼您可以更加確定測試是穩定的——也許您的修復工作有效。 如果沒有,您至少可以更深入地了解片狀測試。

 // Use in build Lodash to repeat the test 100 times Cypress._.times(100, (k) => { it(`typing hello ${k + 1} / 100`, () => { // Write your test steps in here }) })

在 CI 中更深入地了解這種不穩定的測試尤其困難。 要獲得幫助,請查看您的測試框架是否能夠獲取有關您的構建的更多信息。 當涉及到前端測試時,通常可以在測試中使用console.log

 it('should be a Vue.JS component', () => { // Mock component by a method defined before const wrapper = createWrapper(); // Print out the component's html console.log(wrapper.html()); expect(wrapper.isVueInstance()).toBe(true); })

此示例取自 Jest 單元測試,其中我使用console.log獲取被測試組件的 HTML 輸出。 如果您在賽普拉斯的測試運行程序中使用這種日誌記錄功能,您甚至可以在您選擇的開發人員工具中檢查輸出。 此外,在 CI 中使用 Cypress 時,您可以使用插件在 CI 日誌中檢查此輸出。

始終查看測試框架的功能以獲得日誌記錄支持。 在 UI 測試中,大多數框架都提供了截圖功能——至少在失敗時,會自動截圖。 一些框架甚至提供視頻錄製,這對於深入了解測試中發生的事情非常有幫助。

對抗脆弱的噩夢!

重要的是不斷尋找不穩定的測試,無論是從一開始就阻止它們,還是在它們發生時立即調試和修復它們。 我們需要認真對待它們,因為它們可以暗示您的應用程序中的問題。

發現紅旗

當然,首先防止片狀測試是最好的。 快速回顧一下,這裡有一些危險信號:

  • 測試很大,包含很多邏輯。
  • 該測試涵蓋了很多代碼(例如,在 UI 測試中)。
  • 該測試使用固定的等待時間。
  • 該測試取決於之前的測試。
  • 測試斷言不是 100% 可預測的數據,例如 ID、時間或演示數據的使用,尤其是隨機生成的數據。

如果您牢記本文中的指示和策略,您可以在不穩定的測試發生之前阻止它們。 如果他們真的來了,你就會知道如何調試和修復它們。

這些步驟確實幫助我重拾對我們的測試套件的信心。 我們的測試套件目前似乎很穩定。 未來可能會出現問題——沒有什麼是 100% 完美的。 這些知識和這些策略將幫助我處理它們。 因此,我會越來越有信心與那些片狀的測試噩夢作鬥爭

我希望我能夠至少減輕一些你對片狀的痛苦和擔憂!

延伸閱讀

如果您想了解有關此主題的更多信息,這裡有一些簡潔的資源和文章,它們對我有很大幫助:

  • 關於“薄片”的文章,Cypress.io
  • “重試測試實際上是一件好事(如果你的方法是正確的),”Filip Hric,Cypress.io
  • “測試片狀:識別和處理片狀測試的方法,”Jason Palmer,Spotify 研發工程部
  • “Google 的不穩定測試以及我們如何緩解它們,”John Micco,Google 測試博客