End-to-End Testing Web Apps: The Painless Way

Michael Lynch

無痛搞定網頁應用程式的端對端測試

好吧,你抱持懷疑。其他指南曾向你保證可以無痛測試網頁應用程式,結果卻顯示其解決方案需要某種極為特定的技術堆疊或付費的第三方服務。我不會這樣對你。

本指南提供一個簡單直觀且靈活的樣板,讓你能將 End-to-End tests(端對端測試)套用到幾乎任何網頁應用程式。唯一的條件就是你的應用程式能在 Docker 中執行。

真的就只有這個條件!你可以測試 Ruby 應用程式、React 應用程式、Enterprise Java Beans 應用程式,甚至是你自己發明的某個稀奇古怪的網頁技術堆疊。而且不管你是在 Windows、Linux 還是 Mac 上開發都沒關係。最棒的是,你不需要進行繁瑣的設定,也不必安裝 Docker 以外的任何軟體。

本教學使用免費的開源工具,你不需要在任何地方註冊帳號就能執行它們。當你需要在像 Circle 或 Travis 這樣的 continuous integration(持續整合)環境中執行測試時,你不需要做任何特別的設定——你只要用在開發機器上使用的同一行指令來執行測試即可。

Cypress,本場的主角

更新(2022-10-25):我已不再推薦使用 Cypress 來進行網頁應用程式的 End-to-End 測試。對於新專案,我建議改用 Playwright

讓這種測試得以實現的工具是Cypress,它是 browser automation(瀏覽器自動化)領域的新進者。它是一個開源的 End-to-End 測試框架,由一個專職團隊積極開發中。他們的商業模式類似於Docker,兩家公司都發布免費的開源工具,並透過銷售這些工具的代管服務來資助開發。

Cypress 標誌

Cypress 是一款用於自動化網頁應用程式測試的開源工具。

我是在去年看過Gleb Bahmutov(格列布·巴赫穆托夫)在一場地區性軟體研討會上示範 Cypress 後,才第一次發現它的。當他提到 Cypress 完全不依賴Selenium 時,我立刻產生了興趣。我過去所有關於 End-to-End 測試的經驗都非常糟糕,而 Selenium 總是痛苦的根源。

Selenium 標誌

Selenium 是歷史最悠久、也最普及的 browser automation 工具,但它笨重又過時。

Selenium 至今仍是最受歡迎的 browser automation 框架,但它也具備你對一個 15 年前設計的 Java 工具所預期的所有問題。它安裝起來很麻煩,語法彆扭,而且在測試失敗時幾乎無法提供有用的資訊。在格列布·巴赫穆托夫流暢的 Cypress 示範中,它承諾能解決所有這些令人頭痛的問題。

Cypress 標誌

Cypress 的一項亮眼功能是,它會在測試的每個步驟中錄製瀏覽器畫面,協助你診斷失敗原因。

我迫不及待地閱讀了Cypress 文件,卻失望地發現,Cypress 的所有文件幾乎都假設使用者擁有Node.js 技術堆疊,並且是在圖形化環境而非無頭主控台中進行開發。

儘管如此,Cypress 似乎仍前景可期。一年後,我再次關注他們的進展,發現了一個結合 Cypress 與 Docker Compose 的全新範例應用程式。頓時一切豁然開朗。一旦看到 Cypress 在 Docker Compose 下運作,就很清楚該如何將這個模式套用到任何網頁應用程式。今天,我就要向你展示這個模式以及如何在你的應用程式中使用它。

可重複使用的 End-to-End 測試模式

將 Cypress 與 Docker Compose 結合,可產生一個足夠靈活、能套用到幾乎任何網頁應用程式的測試模式。與其他會對你的應用程式實作做出假設的測試工具不同,這個解決方案將你的測試框架與受測的應用程式完全解耦。

Docker 容器架構圖

Docker Compose、Cypress 與網頁應用程式如何協同運作

Docker Compose 讓你可以在一個容器中執行 Cypress,在另一個容器中執行你的應用程式。你的應用程式不需要知道任何關於 Cypress 的資訊,而 Cypress 唯一需要知道關於你的應用程式的,就是用來發送 HTTP 請求的網路連接埠。

一個用來測試的簡易網頁應用程式

作為測試用的範例網頁應用程式,我要介紹 Sentimentalyzer:全世界最笨的文字情緒分析器。它會嘗試從一段使用者寫的文字來猜測使用者的心情。

如果你輸入文字 It's a nice day today,Sentimentalyzer 會推斷你很開心:

在 Sentimentalyzer 中輸入文字Sentimentalyzer 產生結果

Sentimentalyzer 分析開心的文字

如果你輸入文字 Who ate ALL MY WAFFLES?,Sentimentalyzer 會認為你在生氣:

在 Sentimentalyzer 中輸入文字Sentimentalyzer 產生結果

Sentimentalyzer 分析生氣的文字

演算法很簡單:如果超過 50% 的字元是大寫,就代表使用者在大喊,所以一定很生氣。否則,Sentimentalyzer 就會假設使用者心情還不錯。

專案結構

以下是我的範例專案的檔案結構:

main.go               <- source for my web app, Sentimentalyzer
Dockerfile            <- defines how to run Sentimentalyzer in a Docker container
e2e/                  <- folder that contains all the files for my end-to-end tests
  cypress.json        <- Cypress configuration
  docker-compose.yml  <- glue that binds together my app container with the Cypress container
  integration/
    spec.js           <- defines the end-to-end test for Sentimentalyzer

所有的正式環境邏輯都在根目錄中,而所有 End-to-End 測試的程式碼都在 e2e 資料夾中。

在本地端執行 Sentimentalyzer

我刻意不在此處展示應用程式的原始碼,以強調你完全可以在沒看過應用程式本身實作的情況下撰寫 Cypress 測試。Sentimentalyzer 剛好是一個 Go 應用程式,但即使我用 Python 或 Angular 來實作,測試也會是一樣的。如果你感到好奇,原始碼就在GitHub 上

要在你的機器上試玩 Sentimentalyzer,請執行以下指令:

git clone https://github.com/mtlynch/hello-world-cypress.git
cd hello-world-cypress
docker build --tag sentimentalyzer .
docker run \
  --interactive \
  --tty \
  --env PORT=8123 \
  --publish 8123:8123 \
  sentimentalyzer

上述指令會在你的本機上於http://localhost:8123 啟動一個 Sentimentalyzer 伺服器。

現在我能在 Docker 容器中執行我的應用程式了,接下來就可以使用 Cypress 來為它建立 End-to-End 測試。

建立 End-to-End 測試

要撰寫你的第一個 Cypress End-to-End 測試,你只需要三個檔案:

  • cypress.json
  • docker-compose.yml
  • integration/spec.js

cypress.json

此檔案指定了 Cypress 的設定選項

{
  "pluginsFile": false,
  "supportFile": false
}

下載 cypress.json

這些設定沒什麼特別之處,但我將它們設為 false,以防止 Cypress 自動產生不必要的輔助檔案。

docker-compose.yml

此檔案為 Sentimentalyzer 定義一個 Docker 容器,並為 Cypress 定義另一個 Docker 容器,讓它們能夠彼此溝通:

version: "3.2"
services:
  sentimentalyzer:
    build: ../
    environment:
      - PORT=8123
  cypress:
    image: "cypress/included:4.4.0"
    depends_on:
      - sentimentalyzer
    environment:
      - CYPRESS_baseUrl=http://sentimentalyzer:8123
    working_dir: /e2e
    volumes:
      - ./:/e2e

下載 docker-compose.yml

有幾行值得特別說明:

image: "cypress/included:4.4.0"

cypress/includedCypress Docker 映像檔的其中一個系列,其映像檔本身就已預先安裝好 Cypress。其他系列如 cypress/basecypress/browsers 則假設由客戶端在執行時才安裝 Cypress。透過使用 cypress/included 映像檔,我能確保 Cypress 在其容器啟動後立即執行測試。

depends_on:
  - sentimentalyzer

depends_on 區段確保了在 Cypress 開始執行測試之前,Sentimentalyzer 已經啟動並處於運作狀態。

environment:
  - CYPRESS_baseUrl=http://sentimentalyzer:8123

環境變數 CYPRESS_baseUrl 提供 Cypress 可存取 Sentimentalyzer 的 URL。由於 Cypress 與 Sentimentalyzer 在同一個 Docker Compose 設定中執行,Cypress 可以使用其容器名稱(sentimentalyzer)作為主機名稱,向 Sentimentalyzer 發送網路請求。

working_dir: /e2e
volumes:
  - ./:/e2e

最後,我使用 Docker 的磁碟區掛載功能,讓 Cypress Docker 容器能共享主機的部分檔案系統。

主機上 ./e2e 目錄中的所有內容,都會出現在 Docker 容器中路徑為 /e2e 的位置。這確保了當 Cypress 在執行期間寫入日誌、螢幕截圖或影片時,這些檔案會立即在主機上可用,而無需手動從容器複製到主機。以這種方式綁定主機磁碟區,也讓你在無需重建整個 Docker 映像檔的情況下,就能輕鬆編輯並重新執行測試。

working_dir 這一行確保 Cypress 將 /e2e 目錄視為其在檔案系統中的目前資料夾。

integration/spec.js

現在設定已經完成,是時候進入有趣的部分:撰寫測試。

it("detects angry sentiment", () => {
  cy.visit("/analyze");

  cy.get("#feelings").type("I REALLY need some COFFEE");
  cy.get("form").submit();

  cy.get(".results p").should("contain", "You are feeling: Angry");
});

it("detects content sentiment", () => {
  cy.visit("/analyze");

  cy.get("#feelings").type("I think coffee in the morning is just swell!");
  cy.get("form").submit();

  cy.get(".results p").should("contain", "You are feeling: Content");
});

下載 spec.js

即使你不熟悉Cypress API,它的語意也足夠易讀,讓你大概能直觀地理解這些測試。用白話來說,兩個測試都遵循相同的步驟:

  1. 在瀏覽器中,導覽至 Sentimentalyzer 網頁應用程式中的 /analyze 路徑。
  2. 找到文字輸入欄位。
  3. 輸入一些文字。
  4. 提交表單。
  5. 讀取結果。

我將逐行說明第一個測試:

cy.visit("/analyze");

這一行告訴 Cypress 在瀏覽器中載入 Sentimentalyzer 的 /analyze 路徑。Cypress 會將其與我在上方的 docker-compose.yml 中定義的環境變數 CYPRESS_baseUrl 結合,因此完整的 URL 為 http://sentimentalyzer:8123/analyze。你無法從開發機器存取該 URL,但在 Cypress 容器內它是一個有效的位址。

cy.get("#feelings").type("I REALLY need some COFFEE");

接下來,我告訴 Cypress 找到文字欄位。這很簡單,因為該文字欄位有一個唯一的 ID feelings,所以我使用 CSS 選擇器語法來指定元素:#feelings

尋找 feelings 元素的 HTML id

type() 函式會告訴 Cypress 在我指定的欄位中輸入一些文字。

接下來,Cypress 必須提交表單。Cypress 為這項常見任務提供了submit() 函式。頁面上只有一個 <form> 元素,因此用 CSS 選擇器 form 來取得它並提交表單非常簡單:

cy.get("form").submit();

提交表單後,Cypress 應該會進入 Sentimentalyzer 的結果頁面。Cypress 需要檢查文字 "You are feeling: Angry",但這有點棘手,因為包含該文字的 <p> 標籤沒有 ID 屬性:

尋找結果 <p> 標籤的 CSS 選擇器

我再次使用 CSS 選擇器語法,透過指定在 class 為 "results" 的 DOM 節點下的 <p> 元素來定位相關文字:

cy.get(".results p").should("contain", "You are feeling: Angry");

contain 斷言會驗證 <p> 標籤是否包含我預期的文字。

執行我的測試

現在萬事俱備,是時候看看 Cypress 的實際運作了。我用一個簡單的指令來執行測試:

cd e2e
docker-compose up --exit-code-from cypress

旗標 --exit-code-from cypress 會告訴 Docker Compose 將 Cypress 容器的結束代碼作為 docker-compose 指令的結束代碼。這表示當測試通過時,指令的結束代碼為零,而在測試失敗時則為非零的結束代碼。對於使用指令結束代碼來判斷是否成功的建置指令碼或 continuous integration 設定來說,這個行為非常實用。

以下是整個過程在主控台中的樣子:

Cypress 會為每次測試執行建立影片錄製。這是我最喜歡的功能,因為它在診斷測試失敗時有極大的幫助:

Cypress 對 End-to-End 測試的錄影(以 1/4 速度慢放)

測試失敗時的螢幕截圖

上面我向你展示了一個通過的測試。當 Cypress 測試失敗時會發生什麼事?它仍會產生測試執行的影片,但同時也會輸出一張顯示失敗斷言的螢幕截圖:

Cypress 在失敗時產生的螢幕截圖輸出

我的測試失敗時 Cypress 所產生的螢幕截圖(Cypress 預期的是「Furious」一詞,但實際找到的是「Angry」)

這解決了我在使用其他工具時遇到的一個主要痛點。Selenium 支援螢幕截圖,但僅限於斷言之前或之後。該限制導致了令人沮喪的情境:Selenium 宣稱測試失敗,但螢幕截圖卻顯示行為正確,因為瀏覽器狀態在測試失敗後發生了變化。

Cypress 避免了這個問題,因為它的螢幕截圖與斷言是同時發生的。如果測試失敗,螢幕截圖會精確地顯示 Cypress 在失敗當下所看到的畫面。

將此套用到你的網頁應用程式

這三個檔案就是你開始對網頁應用程式進行 End-to-End 測試所需的一切。步驟如下:

  1. e2e 資料夾複製到你的專案中。
  2. docker-compose.ymlsentimentalyzer 的區段替換為你應用程式的 Docker 容器。
  3. 根據你的應用程式 UI 流程重寫 integration/spec.js

原始碼與其他範例

此示範的完整原始碼可在 GitHub 上取得:

我也建立了幾個分支來示範其他常見的 Cypress 情境:

延伸閱讀

本指南提供了 Cypress 的基礎入門。欲了解更進階的功能,請查看官方的 Cypress 文件:

更新(2019-05-02):回應本篇文章,Cypress 團隊發布了已預先安裝 Cypress 的官方 Docker 映像檔。我已修訂本教學以整合他們的新映像檔。請查看Cypress 部落格文章以取得關於其映像檔的更多詳細資訊,以及更多同時使用 Cypress 與 Docker 的技巧。


插圖由 Loraine Yow(蘿蘭·游)繪製。感謝來自 Cypress 團隊的格列布·巴赫穆托夫為本文提供早期回饋。

原文由 Michael Lynch 發布

本文章由 muse-spark-1.2-contributor 進行翻譯