React Unit Test | 為執行細節寫下測試(翻譯)
前言
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 的測試很單純,為什麼還要制定那些規則(注三),讓測試這件事情變得困難?
為什麼測試執行細節是不好的?
關於測試執行細節的壞處,以下舉出兩個明顯且重要的因素(注四):
- 重構的時候,可能會讓測試案例出現錯誤: False negatives(假錯誤)
- 破壞掉程式碼原有邏輯的時候,測試案例仍然回傳正確的結果: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 ===):
0Received:
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 的名稱是 openIndex
、openIndexs
或是 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 的語法(getByText
和queryByText
),確認資料是否正確顯示在使用者面前(或確保它不會顯示),接著再點擊某個 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 不會!
結論
所以該怎麼避免測試到細節?使用對的工具是成功的一半!幾個禮拜之前我歸納了以下幾個重點來理解該怎麼進行測試,它可以幫助你在測試時持有正確的心態,如此一來,自然就能夠學會避免測試到細節:
- 哪部分沒有測試的程式碼一旦發生錯誤,你可能就得開始準備找份新工作(例如結帳流程)
- 試著把他們縮小成一個或幾個單元(按下結帳按鈕後,會將 items 中的資料透過 API
/checkout
送出請求) - 考慮使用者在這個單元的程式內在乎什麼(開發者要能 Render 的結帳表單,末端使用者在乎能夠點擊按鈕)
- 為使用者寫下使用說明,並手動測試確認功能正常(在購物車裡給假的資料 Render 出結帳表單,確保使用該數據送出模擬的
/checkout
請求,並回傳模擬的成功回應,確認畫面上是否正確顯示成功的訊息) - 將使用說明轉換成測試案例,寫進自動化測試。
我希望這對你有幫助!如果你真的想要提高測試技能到另一個水平,那我強烈建議你從 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
而出錯哦!
終於翻完了,此刻的我真的感動到要哭了 😭,沒想到這篇文章翻起來那麼不容易,主要是作者他前半段不斷地強調,測試細節容易產生無效測試,後半段則是告訴大家使用者根本不在乎程式的執行細節,又何苦要寫下關於細節的測試?令我印象最深刻的一句話是
我不想要只是為自己寫下測試案例
測試的目的就是為了讓程式能夠在生產環境下正確運行,而生產環境下會使用到的人,就是末端使用者和開發使用者,作者一直一直在告訴我們這些事情,如果我們的測試案例不貼近使用者的操作,那測試到底是為了什麼?是那個根本不存在的測試使用者嗎?
這篇文章雖然花了我一整天的時間,但是真的翻得很值得!非常推薦大家花時間閱讀!
最後因為本人的英文其實沒有很好,所以如果文中有任何翻譯錯誤或是不正去的地方,再麻煩留言告訴我!非常感謝 🙇♂️