編寫 Cypress 測試的 1,000 英尺概述 #frontend@twiliosendgrid

已發表: 2020-10-03

在 Twilio SendGrid,我們編寫了數百個賽普拉斯端到端 (E2E) 測試,並且隨著不同 Web 應用程序和團隊發布新功能,我們繼續編寫更多測試。 這些測試涵蓋了整個堆棧,驗證客戶在我們的應用程序中推送新代碼更改後會經歷的最常見用例是否仍然有效。

如果您想先退後一步,閱讀更多關於如何總體上考慮 E2E 測試的信息,請隨時查看此博客文章,並在您準備好後返回此內容。 這篇博文並不要求您成為 E2E 測試方面的專家,但它有助於保持正確的心態,因為您會明白為什麼我們在測試中以某種方式做事。 如果您正在尋找向您介紹賽普拉斯測試的分步教程,我們建議您查看賽普拉斯文檔 在這篇博文中,我們假設您之前可能已經看過或編寫過許多 Cypress 測試,並且很想知道其他人如何為自己的應用程序編寫 Cypress 測試。

在編寫了大量賽普拉斯測試之後,您將開始注意到自己使用類似的賽普拉斯函數、斷言和模式來完成您需要的事情。 我們將向您展示我們使用或使用賽普拉斯之前使用或完成的最常見的部分和策略,以針對單獨的環境(例如 dev 或 staging)編寫測試。 我們希望這 1,000 英尺長的關於我們如何編寫賽普拉斯測試的概述能夠為您提供與您自己的比較的想法,並幫助您改進處理賽普拉斯測試的方式。

大綱:

  1. 賽普拉斯 API 綜述
  2. 與元素交互
  3. 斷言元素
  4. 處理 API 和服務
  5. 使用 cy.request(...) 發出 HTTP 請求
  6. 使用 cy.task() 創建可重用插件
  7. 使用 cy.server() 和 cy.route() 模擬網絡請求
  8. 自定義命令
  9. 關於頁面對象
  10. 選擇不使用 window.Cypress 檢查運行客戶端代碼
  11. 處理 iframe
  12. 跨測試環境標準化

賽普拉斯 API 綜述

讓我們從最常用於 Cypress API 的部分開始。

選擇元素

選擇 DOM 元素的方法有很多種,但您可以通過這些 Cypress 命令完成大部分需要執行的操作,並且通常可以在這些命令之後鏈接更多操作和斷言。

  • 使用cy.get(“[data-hook='someSelector']”)cy.find(“.selector”)獲取基於某些 CSS 選擇器的元素。
  • 根據某些文本選擇元素,例如cy.contains(“someText”)或使用包含某些文本的特定選擇器獲取元素,例如cy.contains(“.selector”, “someText”)
  • 讓父元素在“內部”查看,因此您未來的所有查詢都將作用於父元素的子元素,例如cy.get(“.selector”).within(() => { cy.get(“.child”) })
  • 查找元素列表並查看“每個”以執行更多查詢和斷言,例如cy.get(“tr”).each(($tableRow) => { cy.wrap($tableRow).find('td').eq(1).should(“contain”, “someText” })
  • 有時,元素可能不在頁面的視圖中,因此您需要先將元素滾動到視圖中,例如cy.get(“.buttonFarBelow”).scrollIntoView()
  • 有時您需要比默認命令超時更長的超時,因此您可以選擇添加{ timeout: timeoutInMs }cy.get(“.someElement”, { timeout: 10000 })

與元素交互

這些是我們在賽普拉斯測試中發現的最常用的交互。 有時,您需要在這些函數調用中添加{ force: true }屬性以繞過對元素的一些檢查。 當元素以某種方式被覆蓋或從外部庫中派生出來時,通常會發生這種情況,而您對其呈現元素的方式沒有太多控制權。

  • 我們需要點擊很多東西,比如模態框、表格等中的按鈕,所以我們執行cy.get(“.button”).click()之類的操作。
  • 在我們的 Web 應用程序中隨處可見用於填寫用戶詳細信息和其他數據字段的表單。 我們使用cy.get(“input”).type(“somekeyboardtyping”)這些輸入,我們可能需要先清除輸入的一些默認值,例如cy.get(“input”).clear().type(“somenewinput”) 當您執行cy.get(“input”).type(“text{enter}”)時,還有一些很酷的方法可以輸入其他鍵,例如{enter}作為 Enter 鍵。
  • 我們可以與cy.get(“select”).select(“value”)等選擇選項和cy.get(“.checkbox”).check()等複選框進行交互。

斷言元素

這些是您可以在賽普拉斯測試中使用的典型斷言,以確定頁面上是否存在內容正確的內容。

  • 要檢查頁面上是否顯示內容,您可以在cy.get(“.selector”).should(“be.visible”)cy.get(“.selector”).should(“not.be.visible”)
  • 要確定 DOM 元素是否存在於標記中的某處並且您不一定關心元素是否可見,您可以使用cy.get(“.element”).should(“exist”)cy.get(“.element”).should(“not.exist”)
  • 要查看一個元素是否包含某些文本,您可以在cy.get(“button”).should(“contain”, “someText”)cy.get(“button”).should(“not.contain”, “someText”)
  • 要驗證輸入或按鈕是否被禁用或啟用,您可以這樣斷言: cy.get(“button”).should(“be.disabled”)
  • 要斷言是否檢查了某些內容,您可以進行測試,例如cy.get(“.checkbox”).should(“be.checked”)
  • 您通常可以依賴更具體的文本和可見性檢查,但有時您必須依賴類檢查,例如cy.get(“element”).should(“have.class”, “class-name”) 還有其他類似的方法可以使用.should(“have.attr”, “attribute”)來測試屬性。
  • 將斷言鏈接在一起通常很有用,例如cy.get(“div”).should(“be.visible”).and(“contain”, “text”)

處理 API 和服務

在處理您自己的與電子郵件相關的 API 和服務時,您可以使用cy.request(...)向帶有 auth 標頭的後端端點發出 HTTP 請求。 另一種選擇是您可以構建可以從任何規範文件調用的cy.task(...)插件,以涵蓋其他功能,這些功能可以在具有其他庫的 Node 服務器中最好地處理,例如連接到電子郵件收件箱並查找在返回一些值供測試使用之前,匹配電子郵件或對某些 API 調用的響應和輪詢有更多控制。

使用 cy.request(...) 發出 HTTP 請求

您可以使用cy.request()向後端 API 發出 HTTP 請求,以在測試用例運行之前設置或刪除數據。 您通常會傳入端點 URL、HTTP 方法(例如“GET”或“POST”)、標頭,有時還會傳入要發送到後端 API 的請求正文。 然後,您可以將其與.then((response) => { })鏈接,以通過“status”和“body”等屬性訪問網絡響應。 此處演示了進行cy.request()調用的示例。

有時,您可能不關心cy.request(...)在測試運行前的清理過程中是否會失敗並顯示 4xx 或 5xx 狀態碼。 您可以選擇忽略失敗狀態代碼的一種情況是,當您的測試發出 GET 請求以檢查項目是否仍然存在並已被刪除時。 該項目可能已被清理,GET 請求將失敗並顯示 404 not found 狀態代碼。 在這種情況下,您將設置另一個選項failOnStatusCode: false ,這樣您的賽普拉斯測試在運行測試步驟之前就不會失敗。

使用 cy.task() 創建可重用插件

當我們想要更靈活地控制可重用功能以通過 Node 服務器與另一個服務(例如電子郵件收件箱提供商)通信時(我們將在以後的博客文章中介紹這個示例),我們希望提供我們自己的額外功能和對 API 調用的自定義響應讓我們鏈接並應用到我們的賽普拉斯測試中。 或者,我們喜歡在 Node 服務器中運行一些其他代碼——我們經常為它構建一個cy.task()插件。 我們在模塊文件中創建插件函數並將它們導入到plugins/index.ts中,我們在其中使用運行函數所需的參數定義任務插件,如下所示。

這些插件可以在您的規範文件中的任何位置使用cy.task(“pluginName”, { ...args })調用,並且您可以期望發生相同的功能。 然而,如果您使用cy.request() ,除非您將這些調用本身包裝在頁面對像或幫助文件中以便在任何地方導入,否則您的可重用性會降低。

另一個需要注意的是,由於插件任務代碼是在 Node 服務器中運行的,因此您不能在Cypress.env(“apiHost”)cy.getCookie('auth_token')等這些函數中調用常用的 Cypress 命令。 除了請求正文需要與您的後端 API 對話時,您還可以將諸如身份驗證令牌字符串或後端 API 主機之類的內容傳遞給插件函數的參數對象。

使用 cy.server() 和 cy.route() 模擬網絡請求

對於需要難以重現的數據的賽普拉斯測試(例如頁面上重要 UI 狀態的變化或處理較慢的 API 調用),賽普拉斯需要考慮的一項功能是排除網絡請求。 如果您使用 vanilla XMLHttpRequest、axios 庫或 jQuery AJAX,這適用於基於 XmlHttpRequest (XHR) 的請求。 然後,您將使用cy.server()cy.route()來偵聽路由以模擬您想要的任何狀態的響應。 這是一個例子:

另一個用例是一起使用cy.server()cy.route()cy.wait()來偵聽並等待網絡請求完成,然後再執行下一步。 通常,在加載頁面或在頁面上執行某種操作後,直觀的視覺提示將表明某事已完成或準備好讓我們斷言和採取行動。 對於沒有這種可見提示的情況,您可以像這樣顯式地等待 API 調用完成。

一個大問題是,如果您使用 fetch 處理網絡請求,您將無法模擬網絡請求或等待它們以相同的方式完成。 您需要一種解決方法,用 XHR polyfill 替換普通的window.fetch ,並在按照這些問題中記錄的那樣運行測試之前執行一些設置和清理步驟 從賽普拉斯 4.9.0 開始,還有一個experimentalFetchPolyfill性的 FetchPolyfill 屬性可能對您有用,但總的來說,我們仍在尋找更好的方法來處理我們的應用程序中跨 fetch 和 XHR 使用的網絡存根,而不會造成任何破壞。 從 Cypress 5.1.0 開始,有一個很有前途的新cy.route2()函數(請參閱 Cypress 文檔)用於 XHR 和 fetch 請求的實驗性網絡存根,因此我們計劃升級我們的 Cypress 版本並進行試驗,看看是否它解決了我們的問題。

自定義命令

與 WebdriverIO 等庫類似,您可以創建全局自定義命令,這些命令可以在您的規範文件中重複使用和鏈接,例如在測試用例運行之前通過 API 處理登錄的自定義命令。 一旦您在諸如support/commands.ts之類的文件中開發它們,您就可以訪問諸如cy.customCommand()cy.login()之類的函數。 編寫用於登錄的自定義命令如下所示。

關於頁面對象

頁面對像是選擇器和函數的包裝器,可幫助您與頁面交互。 您不需要構建頁面對象來編寫測試,但最好考慮封裝對 UI 的更改的方法。 您希望通過將事物組合在一起來使您的生活更輕鬆,以避免在多個文件中而不是在一個地方更新選擇器和交互。

您可以定義一個具有通用功能的基本“頁面”類,例如open()以供繼承的頁麵類共享和擴展。 派生的頁麵類為選擇器和其他輔助函數定義自己的 getter 函數,同時通過super.open()等調用重用基類的功能,如下所示。

選擇不使用 window.Cypress 檢查運行客戶端代碼

當我們使用 CSV 等自動下載文件測試流程時,下載通常會通過凍結測試運行來破壞我們的賽普拉斯測試。 作為一種折衷方案,我們主要想通過添加window.Cypress檢查來測試用戶是否可以達到正確的下載成功狀態,而不是在我們的測試運行中實際下載文件。

在 Cypress 測試運行期間,會在瀏覽器中添加一個window.Cypress屬性。 在您的客戶端代碼中,您可以選擇檢查窗口對像上是否沒有 Cypress 屬性,然後照常執行下載。 但是,如果它在賽普拉斯測試中運行,請不要實際下載該文件。 我們還利用檢查window.Cypress屬性來檢查我們在 Web 應用程序中運行的 A/B 實驗。 我們不想從 A/B 實驗中添加更多的不穩定和非確定性行為,這可能會向我們的測試用戶展示不同的體驗,因此我們首先檢查該屬性不存在,然後運行下面突出顯示的實驗邏輯。

處理 iframe

使用 Cypress 處理 iframe 可能很困難,因為沒有內置的 iframe 支持。 有一個正在運行的 [問題]( https://github.com/cypress-io/cypress/issues/136 ) 充滿了處理單個 iframe 和嵌套 iframe 的解決方法,這取決於您當前的賽普拉斯版本可能會或可能不會起作用或您打算與之交互的 iframe。 對於我們的用例,我們需要一種方法來處理暫存環境中的 Zuora 計費 iframe,以驗證電子郵件 API 和營銷活動 API 升級流程。 我們的測試涉及在我們的應用程序中完成對新產品的升級之前填寫示例賬單信息。

我們創建了一個cy.iframe(iframeSelector)自定義命令來封裝處理 iframe。 將選擇器傳遞給 iframe 將檢查 iframe 的正文內容,直到它不再為空,然後返回正文內容以將其與更多賽普拉斯命令鏈接,如下所示:

使用 TypeScript 時,您可以在index.d.ts文件中像這樣輸入 iframe 自定義命令:

為了完成我們測試的計費部分,我們使用 iframe 自定義命令來獲取 Zuora iframe 的正文內容,然後選擇 iframe 中的元素並直接更改它們的值。 我們以前在使用cy.find(...).type(...)和其他替代方法時遇到問題,但幸運的是,我們通過更改輸入值找到了解決方法,並直接使用調用命令(即cy.get(selector).invoke('val', 'some value') 您還需要cypress.json配置文件中的”chromeWebSecurity”: false以允許您繞過任何跨域錯誤。 下面提供了其與填充選擇器一起使用的示例片段:

跨測試環境標準化

在使用前面強調的最常見的斷言、函數和方法使用 Cypress 編寫測試之後,我們能夠運行測試並讓它們在一個環境中通過。 這是很好的第一步,但我們有多個環境來部署新代碼並測試我們的更改。 每個環境都有自己的一組數據庫、服務器和用戶,但我們的賽普拉斯測試應該只編寫一次以使用相同的一般步驟。

為了在最終將更改部署到生產環境之前針對多個測試環境(例如開發、測試和暫存)運行賽普拉斯測試,我們需要利用賽普拉斯添加環境變量和更改配置值的能力來支持這些用例。

要針對不同的前端環境運行測試

您需要更改通過 Cypress.config(“baseUrl”) 訪問的“baseUrl” Cypress.config(“baseUrl”) ,以匹配那些 URL,例如https://staging.app.comhttps://testing.app.com 這會更改所有cy.visit(...)調用的基本 URL,以將其路徑附加到。 有多種設置方法,例如在運行賽普拉斯命令之前設置CYPRESS_BASE_URL=<frontend_url>或設置--config baseUrl=<frontend_url>

要針對不同的後端環境運行測試

您需要知道 API 主機名,例如https://staging.api.comhttps://testing.api.com ,才能在“apiHost”等環境變量中設置並通過Cypress.env(“apiHost”) 這些將用於您的cy.request(...)調用以向某些路徑(如“<apiHost>/some/endpoint”)發出 HTTP 請求,或作為另一個參數傳遞給您的cy.task(...)函數調用屬性來知道要訪問哪個後端。 這些經過身份驗證的調用還需要知道您最有可能通過cy.getCookie(“auth_token”)存儲在 localStorage 或 cookie 中的身份驗證令牌。 確保此身份驗證令牌最終作為“授權”標頭的一部分或通過其他方式作為您請求的一部分傳入。 有多種方法可以設置這些環境變量,例如直接在cypress.json文件中或在--env命令行選項中,您可以在 Cypress 文檔中引用它們

要登錄到不同的用戶或使用不同的元數據:

既然您知道如何處理多個前端 URL 和後端 API 主機,那麼您如何處理不同用戶的登錄呢? 您如何根據環境使用不同的元數據,例如與域、API 密鑰和其他可能在測試環境中唯一的資源相關的內容?

讓我們從創建另一個名為“testEnv”的環境變量開始,它可能的值是“testing”和“staging”,這樣您就可以用它來判斷要在測試中應用哪個環境的用戶和元數據。 使用“testEnv”環境變量,您可以通過幾種方式來解決這個問題。

您可以在fixtures文件夾下創建單獨的“staging.json”、“testing.json”和其他環境JSON文件,並根據“testEnv”值如cy.fixture(`${testEnv}.json`).then(...) 但是,您不能很好地輸入 JSON 文件,並且在語法和寫出每個測試所需的所有屬性時存在更多錯誤空間。 JSON 文件也離測試代碼更遠,因此您在編輯測試時必須管理至少兩個文件。 如果所有環境測試數據都直接在cypress.json中的環境變量中設置,則會出現類似的維護問題,並且在過多的測試中管理太多。

另一種選擇是在規範文件中創建一個測試夾具對象,該對象具有基於測試或暫存的屬性,以加載該測試的用戶和特定環境的元數據。 由於這些是對象,您還可以圍繞測試夾具對象定義更好的通用 TypeScript 類型,以便所有規範文件重用並定義元數據類型。 您將調用Cypress.env(“testEnv”)來查看您正在運行的測試環境,並使用該值從整個測試夾具對像中提取相應環境的測試夾具,並在您的測試中使用這些值。 下面的代碼片段總結了測試夾具對象的一般概念。

將“baseUrl”賽普拉斯配置值、“apiHost”後端環境變量和“testEnv”環境變量一起應用,我們可以讓賽普拉斯測試在多個環境中工作,而無需添加多個條件或單獨的邏輯流程,如下所示。

讓我們退後一步,看看如何讓自己的 Cypress 命令通過 npm 運行。 類似的概念可以應用於您可能用於您的應用程序的 yarn、Makefile 和其他腳本。 您可能希望定義“打開”和“運行”命令的變體,以與賽普拉斯“打開”GUI 並在無頭模式下針對package.json中的各種前端和後端環境“運行”保持一致。 您還可以為每個環境的配置設置多個 JSON 文件,但為簡單起見,您將看到帶有內聯選項和值的命令。

您會在package.json腳本中註意到,您的前端“baseUrl”範圍從本地啟動應用程序時的“http://localhost:9001”到部署的應用程序 URL,例如“ https://staging.app”。 com ”。 您可以設置後端“apiHost”和“testEnv”變量來幫助向後端端點發出請求並加載特定的測試夾具對象。 當您需要使用錄製密鑰在 Docker 容器中運行測試時,您還可以創建特殊的“cicd”命令。

一些外賣

當涉及到選擇元素、與元素交互以及斷言頁面上的元素時,您可以使用一小部分賽普拉斯命令(如cy.get()cy.contains()編寫許多賽普拉斯測試。 .click() , .type() , .should('be.visible')

還有一些方法可以使用 cy.request() 向後端 API 發出 HTTP 請求,使用cy.request()在節點服務器中運行任意代碼,以及使用cy.task() cy.server()cy.route()存根網絡請求. 您甚至可以創建自己的自定義命令,例如cy.login()來幫助您通過 API 登錄用戶。 所有這些都有助於在測試運行之前將用戶重置到正確的起點。 將這些選擇器和函數完全包裝在一個文件中,您已經創建了可重用的頁面對像以在您的規範中使用。

為了幫助您編寫在多個環境中通過的測試,請利用環境變量和包含環境特定元數據的對象。

這將幫助您在賽普拉斯規範中使用不同的數據資源運行不同的用戶集。 單獨的賽普拉斯 npm 命令(如package.json中的npm run cypress:open:staging )將加載正確的環境變量值並針對您選擇運行的環境運行測試。

這總結了我們對編寫 Cypress 測試的一千英尺概述。 我們希望這為您提供了在您自己的賽普拉斯測試中應用和改進的實際示例和模式。

有興趣了解有關 Cypress 測試的更多信息嗎? 查看以下資源:

  • 編寫 E2E 測試時要考慮什麼
  • TypeScript 包含 Cypress 測試中的所有內容
  • 在 Cypress 測試中處理電子郵件流
  • 配置、組織和整合賽普拉斯測試的想法
  • 將 Cypress 測試與 Docker、Buildkite 和 CICD 集成