React Unit Test | 為執行細節寫下測試(翻譯)

神Q超人
Enjoy life enjoy coding
12 min readNov 23, 2019
Photo by Alexander Andrews on Unsplash(注一)

前言

Hi!大家好,我是神 Q 超人!這篇是翻譯文章,原文來自於 Testing Implementation Details,是 react-testing-library 的開發者 Kent C. Dodds 所撰寫的,文中舉出幾個例子,描述當你在測試時,如果為 Component 的任何一個你所能察覺的每個細節寫下測試,會為測試本身帶來哪些問題!相信讀完這篇文章,會更能理解 Kent C. Dodds 在創造 react-testing-library 的時候思考了什麼!

為每個細節寫下測試,正是所有災難的根源。為什麼會這麼說?這代表什麼?

在去年(注二)的時候我使用 Enzyme (就和當時的所有人一樣),並仔細地研究 Enzyme 中的某些特定 API。

我完全避免去使用 shallow rendering(淺層渲染),也不曾呼叫 instance()state() 或是 find('ComponentName') 等方法,然後當我為其他人的 PR(Pull Request) 做 code review 的時候,我一再強調「為什麼要避免使用這些 API?」這個原則的重要性。

原因是因為這些 API 讓你能對 Component 的任何一個執行的細節做測試,通常他們會再問說「到底什麼是執行的細節?」

意思是無法以 Component 該有的樣子做測試!

是的,其實 Component 的測試很單純,為什麼還要制定那些規則(注三),讓測試這件事情變得困難?

為什麼測試執行細節是不好的?

關於測試執行細節的壞處,以下舉出兩個明顯且重要的因素(注四):

  1. 重構的時候,可能會讓測試案例出現錯誤: False negatives(假錯誤)
  2. 破壞掉程式碼原有邏輯的時候,測試案例仍然回傳正確的結果:False positives (假正確)

讓我們依序看一下這些以 Accordion Component 所展示的測試例子:

然後我們為這個 Component 測試執行細節的部分:

如果你們曾經寫下和上方例子一樣的測試案例,請舉起你們的手(🙌)

好的!那現在讓我們看看使用這些測試案例會出現什麼問題…

當重構時踫上 False negatives

令人驚訝的,大多人都對測試感到厭惡,特別是對 UI 的測試。討厭的理由各式各樣,但在其中我一次又一次聽見的最重要原因是

「維護測試花費太多時間了!」

「任何時候,我只要修改一些程式,原本正確的測試案例就會爆炸!」這個理由拖垮生產的效率,來看看我們的測試案例為什麼會淪陷於這個令人挫敗的問題。

假設我要開始重構 Accordion,重構完後準備讓它可以同時開啟多個 Item,在重構時,我並沒有去改變 Accordion 原本的行為,只是改變了實現原有行為的過程,如下方的修改:

太棒了!我們手動檢查一下修改後的 Accordion 確認一切都正常運作,接下來要加入可以同時開啟多個 Item 的新功能就是小事一樁了!但假如我們運行 Accordion 的測試案例…💥 碰 💥一聲!測試彈出失敗!哪個測試案例失敗了?是 setOpenIndex sets the open index state properly

錯誤訊息呢?

expect(received).toBe(expected)Expected value to be (using ===):
0
Received:
undefined

這個測試失敗的警告有告訴我們真正的問題嗎?不 Accordion 執行的行為仍然正確!

這就是所謂的 False negatives,意思是雖然我們測試失敗,但是

失敗的原因來自於測試案例,而不是程式本身的行為!

老實說,對測試失敗而言,我無法想像到更煩人的情況,但算了,讓我們修復測試案例吧:

在上方的例子中,我們了解了在「對執行細節做的測試」的程式進行重構有可能會出現 False negatives 的狀況,也導致脆弱且令人沮喪的測試案例。

False positives

那現在轉換個場景,當你的同事在 Accordion 的 Component 下看到這段程式:

<button onClick={() => this.setOpenIndex(index)}>
{item.title}
</button>

他盯著螢幕對自己說「這個在 render 內宣告的 arrow fuction 似乎會造成 效能問題,雖然有點早,但也許該做個優化!」他心裡想只是修改 onClick 的呼叫, Accordion 應該不會出錯,因此就將 arrow function 抽出來,並運行測試:

<button onClick={this.setOpenIndex}>
{item.title}
</button>

太帥了!測試結果完全正常 ✅!他不需要在瀏覽器上確認功能是否正常就可以 commit,因為你對測試案例非常有信心,對吧?但這個 commit 被混合在與它無關的 PR(Pull Request)中,而這個 PR 裡異動了上千行的程式碼,意思是這個為了效能所做的修改被埋沒其中。

結果在測試案例運行良好的狀況下, Accordion 的成品居然沒有辦法來得及給客戶!Nancy 無法獲得五月天台北小巨蛋跨年場的門票(注五),感到非常難過一直哭,整個團隊都覺得糟透了!

那麼到底出了什麼問題?難道我們沒有對 setOpenIndex 進行測試,確認他到底有沒有修改 State 嗎?不!是有的!測試結果也回報成功不是嗎?但問題在於…

沒有測試按鈕是否能夠正確使用 setOpenIndex!(注六)

這種情況就被稱作 False Positives,它使我們在應該要失敗的時候無法測出失敗的結果,該怎麼做才不會再發生這樣子的狀況?也許要另外增加一個測試案例,確認點擊按鈕後 State 會不會正確更新,然後我們還需要把測試的覆蓋率增加到百分之百,來避免慘劇重演。而且我還應該要再寫一打左右的 ESlint 插件,確保不會有人再使用那些 API 來測試細節。

…但我不會去打擾…呃(注七),我只是對於那些無效的測試(False negatives False Positives)感到厭煩,如果要一直面對這些問題,我寧願不寫測試,

甚至是刪除所有測試!

如果擁有一個工具可以讓我們直接 掉入成功之坑,那不是很好嗎?沒錯!這麼好的工具這裡剛好有一個!

不再測試執行細節

我們可以用 Enzyme 重寫所有測試,然後限制自己不去使用那些實現細節的 API,但我反而會選擇使用 react-testing-library,這個套件使我很難在測試中看見並使用細節,來看看它的測試怎麼寫:

看起來很棒!這個測試證明了 Accordion 的行爲是正確的!而且不論 State 的名稱是 openIndexopenIndexs 或是 tacosAreTasty 🌮(注八),測試都會通過,這使測試出現 False negative 的狀況不會再發生。然後如果我用錯誤的方式操作 setOpenIndex,那測試案例就會出現錯誤!一切都太美好了!因為不只是 False negative,就連 False positives 出現的可能性也歸零囉!

更開心的是我不需要再熟記那些無聊的規則(注九),也不需要再安裝一堆 ESLint 的插件避免測試到細節,完完全全只是以一般用法使用 react-testing-library,測試案例的結果就能給我十足的信心,也能讓 Accordion 在使用者手上正常運作!

所以…測試細節到底是什麼?

我對細節所能想到最簡單的定義是:

通常都是你的使用者所不會使用、看見、甚至是了解的東西。

首先,我們需要思考的第一個問題是「你的程式會被誰使用?」我想答案應該很明顯,那些會在瀏覽器上點點按按的末端使用者肯定是其中之一,他們會去注意 Render 在畫面上的按鈕或其他內容,並觀察點擊按鈕後會發生的事。

但除此之外還有開發人員,他們會用來傳入 Props 使用 Accordion,期望它能夠 Render 出對應的畫面(在我們的例子裡是傳入 items Render 出 list),所以 React 的 Component 有兩種代表性的使用者,分別為末端使用者和使用 Component 的開發者,兩個都是我們在開發時需要考慮的使用者。

那這兩種用戶分別會使用、看見或是了解程式的哪些部分?在 Component 中末端使用者會看見 Render 後的畫面,並與它做互動,而開發者會看見傳入的 Props 與 Component 做互動,所以我們應該只需要對「能不能正確使用 Props」及「Render 出來的畫面」這兩項代表性的指標做測試。

這也是上方使用 react-testing-library 的例子所做的,在例子裡先傳入一個假的 Props 給 Accordion,然後透過查詢 Element 的語法(getByTextqueryByText),確認資料是否正確顯示在使用者面前(或確保它不會顯示),接著再點擊某個 Item,確認資料會不會重新 Render 對應的畫面(應該要展開該 Item 的內容)。

現在回來思考 Enzyme,在使用 Enzyme 的測試案例中,我們可以直接取到 openIndex 這個 State 的值,但 openIndex 的值並不是使用者直接在乎的,他們不曉得那是什麼,更不會知道裡面是儲存 String 或是 Array,也不會想要了解 setOpenIndex 這個方法,

與其說不知道或不想了解,坦白講是根本不在乎!

但在這種情況,我們在 Enzyme 中卻還是對細節做測試。

這就是為什麼我們用 Enzyme 做的測試容易發生 False negatives因為測試案例與末端使用者和開發者在做的事情完全沒有關係,就像是我們站在測試的角度建立了第三種使用者:「只在乎測試的測試者」,而這種使用者也不會有人在乎他,

因為我們的程式碼從來就不是為了測試存在!

這麼做太浪費時間了,而且我不想要只是為自己寫下測試案例,自動化測試的目的應該是為了讓程式能夠在生產環境下正確運行!

你的測試案例越貼近使用者的使用方式,測試結果就能給你越大的信心Kent C. Dodds

閱讀更多關於 避免創造測試使用者

最後! React Hooks 讓你感到興奮嗎?如果你用 Hooks 重寫 Accordion 的話,原本用 Enzyme 寫的測試案例就會出錯(注十),但是 react-testing-library 不會!

來自原文的 gif

結論

所以該怎麼避免測試到細節?使用對的工具是成功的一半!幾個禮拜之前我歸納了以下幾個重點來理解該怎麼進行測試,它可以幫助你在測試時持有正確的心態,如此一來,自然就能夠學會避免測試到細節:

  1. 哪部分沒有測試的程式碼一旦發生錯誤,你可能就得開始準備找份新工作(例如結帳流程)
  2. 試著把他們縮小成一個或幾個單元(按下結帳按鈕後,會將 items 中的資料透過 API /checkout 送出請求)
  3. 考慮使用者在這個單元的程式內在乎什麼(開發者要能 Render 的結帳表單,末端使用者在乎能夠點擊按鈕)
  4. 為使用者寫下使用說明,並手動測試確認功能正常(在購物車裡給假的資料 Render 出結帳表單,確保使用該數據送出模擬的 /checkout 請求,並回傳模擬的成功回應,確認畫面上是否正確顯示成功的訊息)
  5. 將使用說明轉換成測試案例,寫進自動化測試。

我希望這對你有幫助!如果你真的想要提高測試技能到另一個水平,那我強烈建議你從 TestingJavaScript.com 獲得專業的執照。

祝你好運!

PS. 如果你想試試文章中的例子,可以到 codesandbox

PS. 課後練習,假設我修改了 AccordionContents 這個 Component 的名稱,那第二個 Enzyme 測試結果會變得如何?(注十一)

注釋專區

注一:在 unsplash 中找不到原圖了,所以用其他圖代替。

注二:這篇文章是在 2018/11/20 寫的,所以作者的去年就是 2017 年。

注三:這些規則原文中沒有另外說明,但我猜想這裡是指說「不能使用會暴露細節的 API」這項規則。

注四:不論是 False negatives 或 False positives 都是指無效測試。

注五:原文是無法拿到 Wicked in Salt Lake 的門票,Wicked 是非常有名的音樂劇,根據估計,在整個劇院任期內,已有 5500 萬人觀看(資料來源)。

注六:這裡沒有正確使用的原因是因為在呼叫時沒有傳入 index

注七:原文是 「… But I’m not going to bother…」感覺得出來作者欲言又止,但猜不出他想表達什麼 😭

注八:看起來很好吃的墨西哥捲餅。作者想表達不論完成功能的細節是啥都沒差。

注九:這裡和注三提到的事一樣。

注十:這篇文章發佈的時候 Enzyme 還不支援 React Hooks 測試 ,但現在似乎已經沒有問題了!Enzyme 的 Hooks 文件

注十一:Enzyme 的第二個測試案例 Accordion renders AccordionContents with the item contents 會因為找不到改名後的 AccordionContents 而出錯哦!

終於翻完了,此刻的我真的感動到要哭了 😭,沒想到這篇文章翻起來那麼不容易,主要是作者他前半段不斷地強調,測試細節容易產生無效測試,後半段則是告訴大家使用者根本不在乎程式的執行細節,又何苦要寫下關於細節的測試?令我印象最深刻的一句話是

我不想要只是為自己寫下測試案例

測試的目的就是為了讓程式能夠在生產環境下正確運行,而生產環境下會使用到的人,就是末端使用者和開發使用者,作者一直一直在告訴我們這些事情,如果我們的測試案例不貼近使用者的操作,那測試到底是為了什麼?是那個根本不存在的測試使用者嗎?

這篇文章雖然花了我一整天的時間,但是真的翻得很值得!非常推薦大家花時間閱讀!

最後因為本人的英文其實沒有很好,所以如果文中有任何翻譯錯誤或是不正去的地方,再麻煩留言告訴我!非常感謝 🙇‍♂️

延伸閱讀

  1. [Day 4]單元測試:是否需針對非 public method 進行測試?
  2. 单元测试 — 私人/保护方法是否应进行单元测试?

--

--