React Unit Test | 讓你的 UI 測試適應變化(翻譯)

神Q超人
Enjoy life enjoy coding
8 min readNov 14, 2019
Photo by Warren Wong on Unsplash

前言

Hi!大家好,我是神 Q 超人,本篇文章原文來自Kent C. Dodds 所經營 Blog 中的 Making your UI tests resilient to change ,這個作者開發了 react-testing-library 專案,替測試 React 開啟了一扇新的大門,而在他的 Blog 裡有記錄了許多對於測試的看法,每一篇都很值得一看,之後也會持續翻譯他的文章,讓更多人認識 React 的測試!並擁抱更多想法!

因為我是第一次翻譯文章,有些不太了解或是需要覺得補充的地方,會在文章內加上「注」,然後統一在文章最下方的時候提出疑問或解釋,這樣也不會影響各位閱讀原文。

使用者介面的測試特別纖細(注一),且容易出現問題,讓我們來看看如何改進它!

你是個開發者,而且想要避免為使用者帶來殘破的登入體驗,所以決定為它寫下測試,以證明登入的操作是良好的。讓我們來看看類似這樣子的登入表單

現在如果要測試這個表單,我們會想要填入 UsernamePassword,最後按下 Login 送出,為了做到這一點,我們需要先把 form Render 出來,然後從 Render 出來的 Document 中找到這些 Element 並操作它。也許你會試著這樣做:

const usernameField = rootNode.querySelector('.username-field')const passwordField = rootNode.querySelector('.password-field')const submitButton = rootNode.querySelector('.btn')

而就是問題所在,當我們添加一個按鈕會發生什麼事情?例如在 Login 前加上 Sign up:

天啊,原本的測試案例會因為只增加一個按鈕就失敗了,但是我們仍然可以這樣輕鬆修復它,對吧:

const submitButton = rootNode.querySelector('.btn')const submitButton = rootNode.querySelectorAll('.btn')[1]

太好了,這樣子看起來還不錯!但如果我們開始改用 CSS-in-JS 的方式替 form 中的 Element 設置 Style,那 Element 就不需要 ClassName 的屬性了,像那些 username-fieldpassword-field 的字眼,這時候我們該移除它們嗎?或是繼續留著?因為測試案例需要它們,嗯…… 🤔

所以我們該怎麼寫下具有適應力的選擇器?

有鑒於「你的測試案例越貼近使用者的使用方式,測試結果就能給你越大的信心」,我們從「使用者根本不會在乎他們操作的 Element 擁有什麼 ClassName」開始思考是不錯的方向。

可以想像一下,當你的團隊中有成員正負責人工測試,而你使用上方那些測試案例想告訴他們這個 form 該怎麼操作時,測試案例會透露哪些訊息?

  1. 取得 ClassName 為 username-field 的 Element

「等等!」他們急著喊停,並提出疑惑:「我該怎麼知道哪個 Element 的 ClassName 是 username-field?」

你泰然自若地回答:「哦!首先打開開發者工具……」

他們沒等你說完,急著插話:「但是使用者不會打開開發者工具!為什麼不單純用 Label 標籤去找到對應的輸入框就好?Label 不是對應到 username 的輸入框嗎?」

「對耶!突破盲腸!」此時,你的眼睛正閃閃發亮。(注二)

這也是爲什麼 Testing Library 擁有獨特查詢 Element 的方法,他可以幫助你用和使用者相同的方式尋找 Element,不論是 labelplaceholdertext contentsalt texttitledisplay valueroletest ID 等屬性,都可以用來找到特定的 Element。

事實上,上方的屬性是依照 推薦度 所排列的,雖然使用這些屬性下去搜尋,你就得另外為程式多下點功夫,像是另外設置一些屬性之類的(注三),但是當你使用新查找 Element 的方式寫下測試案例,並以它為團隊裡的人工測試者說明如何操作時,會變得像這樣:

  1. 在 Label 為 username 所對應的輸入框中輸入假資料
  2. 在 Label 為 password 所對應的輸入框中輸入假資料
  3. 點擊那個顯示文字為 Login 的按鈕(注四)

這麼做有助你讓測試流程看起來就像真實使用者在操作系統。測試也能帶來相對多的價值。

什麼是用 data-testid 查詢?

有時候你仍然無法很可靠的依照 Element 自有的屬性去找到它,因為它們都可能在開發或是維護時改變,針對那些常變動的 Element ,就很推薦使用 data-testid(但也不要忘了 Element 上說不定有其他更適合的屬性(注五))

很多人覺得奇怪,為什麼在那麼多屬性中沒有提供類似 getByClassName 的查找方法,我不喜歡這麼做是因為 ClassName 通常是為了指定 Style 的樣式而存在的,所以當我們為了測試而增加一堆 ClassName,那將來維護時就會讓人更難了解這些 ClassName 的用途到底是什麼,你也不知道什麼時候可以刪掉它們。

而且如果僅僅是增加了同樣 ClassName 名稱的 Element,那就會遇到像上方 Sign up 按鈕的問題,這麼一來不論是重構、添加功能或是其他更動到 UI 的時候,你都必須修改測試案例,

這些更改的理由都在訴說著測試案例太脆弱了

核心問題是測試案例的內容和 UI 之間的關係太不具體,但如果我們能夠透過更明確的屬性來查找 Element,便能克服這個問題。

像是如果我們可以替 Element 增加一些後設資料,並用明確的名稱來註明它是什麼,那就能試著選擇使用它來解決測試案例和 UI 關係不具體的問題,猜猜看是什麼後設資料?其實有個現成的 API,那就是 HTML5 的 Data 屬性(注六),我們可以這樣使用它:

然後你的測試案例就能這樣取得 Element:

const usernameEl = getByTestId('username');

這個技巧和觀念在 E2E(端對端)測試也很有幫助,因此我建議你也使用它,但是有些人會擔心這樣暴露一些資訊會不會有什麼問題,如果你也這樣想,請試著思考這真的會造成什麼問題嗎?說實話,這件事可能沒有你想像的那麼重要,但如果你仍然覺得這是件大問題,也可以使用 babel-plugin-react-remove-properties 這個套件,來讓你在 Build 的時候移除 Data 屬性。

結論

你可以發現,只要我們想像並模擬使用者操作系統的方式寫下測試,不僅可以讓測試更適應 UI 的變化,也能讓測試為你帶來更多價值,如果你想要學習更多關於測試的知識,那建議你可以閱讀我的 Blog 中的 Testing Implementation Details

希望這篇文章能夠幫助到你!祝你好運!

注釋專區

注一:這裡的原文其實是 famously finicky,famously 是出名的,finicky 是挑惕的意思,但是這樣子怎麼翻都很奇怪,而 finicky 還有另一個意思是需要細心的或過分講究的,所以最後就翻成非常纖細,如果有問題麻煩告訴我,感謝各位 🙏

注二:其實那些生動的形容兩人對話的情境在原文是沒有的,原文中只是單純紀錄對話內容而已:

注三:這裡的多下點功夫是指說,有時候我們在打程式的時候,通常不會為 Element 加上專屬於它的註明屬性(也就做測試特徵),所以可能會需要為了測試加上它。

注四:這個地方原文是寫 Sign in,但是範例的 HTML 登入按鈕是 Login,所以就改為 Login,不是原文的 Sign in。

注五:這裡不知道是不是代表說,使用 data-testid 是最後的手段,應該要確認在當前的 Element 中,是不是還有更適合的屬性用來查找 Element。

注六:HTML5 的 Data 屬性介紹:MDN 傳送門

終於結束這輩子第一次的翻譯了,只能說果然還是沒有想像中的簡單,常常不曉得該怎麼用中文將原意更好的表達出來,搞得還是很像硬翻的文章,還請大家見諒,然後因為本人的英文其實沒有很好,所以如果有任何可以改進或是錯誤的地方,再麻煩留言告訴我,感謝各位 🙇‍♂️

--

--