E2E Test 哩賀!


大約在半年多前,Codementor/Arc 正式採用了自動化的 end to end (e2e) test。 在這段時間之內,我們細心的呵護它,從技術上到流程上,都做了無數次的調整(也還正在進行中)。 而它也沒讓我們失望,從小到記不起來的 issue 到大的首頁不能登入的 bug 都幫我們抓到過。

現在回想起來,從一開始的導入到後續的維護,有蠻多波折來回的。在內部的需求彈性很大,同時外部有多種選擇的時候,要做出決定其實是不容易的事情。 我們也感受到有時候”怎麼做決定”比”這次做出什麼決定”更重要,因為如果 “做決定的方法” 本身可以隨著時間被檢討精煉,往後做不同類型的決定的時候也許可以更精準有效率。

在這篇文章裡,我們試圖把一些”做決定的過程”整理出來跟大家分享,希望大家之後如果遇到類似的狀況,可以因此快快跳關:

  • E2E Test 是什麼?
  • 為什麼要/不要導入呢?
  • 怎麼開始呢?
  • 遇到的技術問題
  • 在技術以外的問題

另外,我們也把我們一路上建立的解法整理成一個 starter-kit repo,下面提到的東西大多都在裡面,也歡迎 PR/直接拿去用。

hackday 放上一張前陣子去宜蘭hack day的照片,封面與內容不符

E2E Test (對我們來說)是什麼?

其實客觀來說,e2e test 的定義可能會隨著在不同情境而有改變。對我們來說,我們這邊所謂的 e2e 指的是 > 模擬一個 user,從 UI 上去執行我們預期他要可以完成的任務。 我們希望用自動化的方式,去驗証在任何時候,我們產品中最重要的關鍵任務都是正常運作的。

有別於 unit test,e2e 的 scope 是更大的,更貼近 user 的。所以理論上即使在我們系統的架構大幅改動的狀況下,它也可以確保我們提供給使用者的價值不受影響。但能力越大,責任越大。相對的 e2e 的維護成本和規畫,也要比 unit test 大的多。

做不做?

為什麼不一開始就做 e2e 呢? 在我們正式導入 e2e 之前的很長一段時間,其實我們是靠人力的方式,定期手動去確認我們的關鍵任務(像是 signup, 進行 mentoring session 等)可以正常運作。我們定義了我們認為重要的”關鍵任務” 的腳本,所以雖然說是以人力進行,但整個流程還算是穩定的。

一直以來,我們其實認為以我們的各種狀況來評估,用人力進行UI測試是最有效率的作法,主要的原因是:

  • 我們有還算完整、穩健的 unit test,code review,CI/CD 流程。所以大致上來說,推出去的 code 是有一定品質的。即使發現有什麼 bug,因為上述的基礎建設,要修復 bug 也是很快的事情。
  • 自動化的 E2E 位在 Test Pyramid 的相對上層。這意味著它隨著 UI/流程 的改動所需要的維護成本相對來說是高的。而我們的 UI/流程,在大多數的狀況下是會需要一直演進調整的,所以就算真的把 E2E 做起來,要去維護 test 的成本可能會大於人力測試的成本。
  • 我們有很強力的 support team! 當有使用者告訴我們有重大 issue 的時候,我們有很完善的回報流程,所以可以馬上知道,馬上修好。

這一切都運作得很完美。直到有一天,我們發現我們送去 GA 和我們自己 server 的用戶行為追蹤的事件對不起來,而且好像已經壞很久了…

我要寫個慘字~~

我們才發現,追蹤事件這件事情,巧妙的閃過了我們上述的所有防護網:

hackday

  • 追蹤事件本身的邏輯是有被 unit test 保護到的。但是我們出問題壞掉的地方是在 integrate 的點
  • 追蹤事件是 user 不會有感覺的東西,所以 support team 幫不了我們
  • 即使是定期跑的人工 UI 測試,要去驗証每一個平台的追蹤事件的每一個 attribute 都有準確的記下來,以正常靈長類的能力是有困難的。

而對於 Codementor 來說,如果重要的追蹤數據有誤的話,就好像拿台灣的地圖去美國開車一樣,根本無法做出任何有意義的決定,只能通靈阿!

see

是時候來好好規畫 e2e了

有了之前的考量和現在的痛點,我們希望可以善用這把兩面刃,不要問題沒解決,先把自己切了。於是我們設定了 e2e 的邊界條件:

只把重要的、UI 流程相對穩定的東西放進 e2e 裡面。

接著就要開始選擇實作的工具了

在選擇實作的工具這邊,我們先把所謂”工具”分成兩大塊: 1. 執行的環境:我們想要有某個環境,讓我們可以順利的 deploy e2e 上去。並且在任何我們想要的時候執行。想像就像是 CI/CD 一樣,但是觸發的時機點除了 code push 之外,還可以有任何我們想要的時間,像是 nightly build/手動觸發等。 2. 實際使用的工具:這部份我們可以把它看成 “e2e 的 code 會 call 誰的 API”。可能的選項有 Cypress, Puppeteer 等等。

執行環境

這部份因為我們本來就有熟悉的 CI/CD 環境 (CircleCI),所以優先嘗試它。結果用了之後發現超棒阿很無痛,所以好像也沒有什麼理由不用,於是就選它了。

目前使用它的方法是每次 push 會 trigger build + nightly build

實際使用的工具

這邊的選項就比較多元了。我們的策略是:

  • 先 Google 一輪,找出看起來功能相對完整,社群的支援也健全的選項們。
  • 定義一個 “可以 cover 大多測試工具功能的 test case,然後用上面找到的每一個 solution 都做看看”

第二點的用意在於,在找到若干種選項之後,其實光看文件很難知道這個選項是不是可以符合我們的需求。或更精準地說,因為我們在事前很難窮舉我們”所有”的需要的功能,我們很難確定 “有沒有什麼我們要的功能是這個工具沒有支援的”。 於是我們把各種 solution 的功能區分成兩大類:

  • 身為一個 e2e solution 應該要具備的基本功能
  • 比較偏門的功能

假設大多的 solution 都具備了第一類,這樣一來我們只要找一個我們自己認為最偏門、最特殊的 test case,然後用各種 solution 試著把這個 test case 實作上去試看看,就可以知道以功能來說,選用的 solution 能不能支援了。

在這樣的前提下,我們設定的 test case 是:

當使用者從 “外部網域” 進到我們自己的網域的時候,我們可以正確地把這個行為記下來,並且在正確的時機點送上 server

然後我們分別用下面我們研究出的候選名單去實作看看:

在實做的過程中,我們要求自己如果遇到各種困難,盡可能 “假設我們就是要選這個 solution 不能換了” 用各種 workaround 去滿足我們想到的 test case。 目的是希望透過這樣的方式,感受一下在這個 solution 下要解決問題的難度有多高。包括底層的架構、文件、社群的支援等等。

最後我們選擇了第二個 “Puppeteer + Jest” 這個選項,主要原因是 Cypress 有不能在同一個 test 跨網域的限制。再者我們其實在 e2e 重視 implementation logic 更甚跨瀏覽器的支援,所以只有 chromium 一種也覺得很 ok。當然過程中還有各種技術上的方向的考量,但在這篇文章就先略過了。

遇到的困難們

於是我們做出了技術上的選擇,也有了第一個版本在線上跑了!但接下來我們立刻遇到各種問題,這邊把一些問題和解法大概整理一下:

團隊的學習成本 - 我不太會寫/維護 e2e test

這是我們遇到的第一個問題。雖然說在我們選用的 solution 下,e2e test 是每一個成員都會寫的 JavaScript,但對於實際上寫起來的感覺和節奏還是不太了解。我們解決這個問題的方法是

  • 先把我們認為重要的 “關鍵任務” 整理出來
  • 開一個內部的workshop, 一個人寫一個 test case。Workshop 結束之後,除了大家對於 e2e 都有一定的感覺之外,重要的 test case 也補了不少。

e2e 時不時會有無法重現的錯誤 - 真猜不透你阿!

接下來在有了一定量的 test case 之後,隨之而來的 random failure 立刻造成我們的困擾。這部份我們的做法是:

  • 首先讓 error 變得更好理解,包括在 test 跑完之後把錯誤的 case 截圖下來,把 console history 留起來等。這個部份不可諱言,是 “Puppeteer + Jest” 這個 solution 本身沒有 cover 到的地方,需要多一些力氣去把它兜起來。如果是 Cypress 的話應該是自帶有這類的東西。
  • 接著我們漸漸發現,有些在 e2e 時候遇到的 API 錯誤,因為後端的架構是由多個 service 組成的關係,無法有效率的定義問題的所在。於是我們調整了後端的 error handling 讓這一切比較容易一點。這點嚴格來說和 e2e 沒有關係,只能說是 e2e 讓我們發現了這個本來就存在的問題點。

e2e 該由誰來維護 - 沒空阿阿阿

所謂”維護”,其實包括了兩個部份:

  1. 在 nightly build 回傳 test 過不了的時候,第一時間去檢查是不是可怕的問題。
  2. 真正把問題修好

於是我們做了一個小機器人指派每天的值日生,負責在 nightly build 壞掉的時候指派一個人先去看。並且在看的時候判斷這個 build 壞掉的原因和 action item:

  • Production Code 壞了:馬上手刀告訴大家,立刻把 production 上的東西修好
  • Production Code 修改了,但 test case 沒有跟上:把它記在 doc 上,在 bug bash(註1)的時候處理掉
  • Random Failure:重跑一次就不見的淘氣錯誤。把它記在 doc 上,在 bug bash 的時候處理掉

Production code css class 和 test code 的藕合太深 - Test code 滿滿的 css selector 是要怎麼讀啦

這個部份我們則是用 page driver 的寫法,某種程度上減輕這個問題。作法大致上是在 test code 當中,再進一步加入一個 “window driver” 的這層,把 test case 想驗証的 “商業邏輯” 和 “UI細節” 分開。不囉嗦直接看 code

以上大概是我們遇到比較大的問題,和大家討論過後的解法。

寫在後面

其實在一開始建立 E2E 的時候,只是想要解決 tracking 遇到的問題而已。並且一開始,坦白說其實 false alarm 是大於它為我們抓到的 bug 的。但我印象很深有一次在早上的 sync meeting 討論到它幫我們抓到一個重大的 bug,我們第一次感覺到 “E2E 終於划算了” 的那次。 在那之後,E2E 除了抓到 bug 之外,我覺得某種程度上也像是一面鏡子,反映著我們流程上的缺失,像是 error tracking 等等。畢竟要可以自動化的模擬一個 user 用 UI 操作我們的網站,我們的各種 infrastructure 也要有一定的成熟度才行。

註1 - Bug bash:我們每周會安排一個下午的時間,把一些不是很急但放久會臭酸的 bug 一次修掉。

後面的後面 - We’re Hiring!

如果你對於這些做決定的過程有興趣,想和我們一起面對,還順便想用世界級的產品改變世界的話,請寫信跟我們說!我們在找前端後端,我全都要!