# [FE Sharing: react testing matchers & 測試種類 by Pei wang [toc] ## 前情提要 ### 測試套件 library * @testing-librart/react : 使用 reactDom 渲染元件做測試 * @testing-librart/user-event : 模擬使用者行為,例如click、input、focus * @testing-library/dom : 用來找出透過元件渲染出的元素 * jest : 跑測試、輸出結果 * jsdom : 在 node 環境下模擬 browser * vitest : 因為mini site大多使用vite搭建,可以使用vitest做測試,介面與第三方套件豐富且快速,但這次不多介紹 ### 主要 function 複習 test / screen / render / expect ```javascript import { screen, render } from '@testing-library/react'; import RepositoriesSummary from './RepositoriesSummary'; // test:運行測試的方法 test('display the primary languages of the repository', () => { const repository = { stargazers_count: 1, open_issues: 30, forks: 5, language: 'JavaScript', }; // render:測試環境中建立元件,模擬DOM環境 render(<RepositoriesSummary repository={repository} />); for (let key in repository) { // screen:查找操作測試環境中的元素 const element = screen.getByText(new RegExp(repository[key])); // expect:jest的global function,也是 matchers,用來比對預期結果和實際結果是否相同 expect(element).toBeInTheDocument(); } }); ``` ### Queries 用來查找操作元素 https://testing-library.com/docs/dom-testing-library/cheatsheet No Match | 1 Match | 1+ Match | Await? | | --- | --- | --- | --- | | **getBy** | throw | return | throw | No | | **findBy** | throw | return | throw | Yes | | **queryBy** | null | return | throw | No | | **getAllBy** | throw | array | array | No | | **findAllBy** | throw | array | array | Yes | | **queryAllBy** | \[\] | array | array | No | <hr> ## 前端測試種類 - unit testing:單元測試,針對個別元件的測試,前端來說最常使到的工具是由 Facebook 推出的 [Jest](https://github.com/facebook/jest);後端則很常使用 [mochajs](https://mochajs.org/) 搭配 [chai](https://github.com/chaijs/chai) - integration testing:整合測試,整合不同API或元件互動的整合測試,常見工具以 React 來說最常提到的應該是 Testing Library 搭配 React Testing Library或 enzyme - end to end testing:E2E test,模擬使用者操作測試,前端最常聽到的是 [Cypress](https://www.cypress.io/)、Google 推出的 [Puppeteer](https://github.com/puppeteer/puppeteer)、或是微軟的 [Playwright](https://playwright.dev/) ### example Unit testing without integration testing https://twitter.com/juliehubs/status/1078363114666627072 ### 測試主要概念 不管是怎樣類型種類的測試,都是以「預期結果」和「真實結果」去做比對,確認是否結果會如開發者預期,所以寫測試的前提是要可以知道預期結果,如果連結果都無法想像到的話,就無法進行測試 ### 不同種類測試選擇的考量點 - Unit testing 作為第一道防線,可以最快看出錯誤核心的地方,不會因為某個元件的功能更動壞掉全部元件,但會因為UI頻繁改動,驗證邏輯常需要改 - integration test 和 E2E test 是測試不同元件整合起來的功能,會可能因為畫面改動、API 回傳資料錯誤而結果失敗,除了會檢查欄位是否有填,也會檢查漏填時,彈出的錯誤訊息 - 不同測試容易發現不同問題,整合測試可以知道各個模組之間交互情況,例如個別元件都可以正常運作,但交互作用就會出現問題,就需要透過整合測試才會得知。或是在使用者操作下才會出現的問題,例如點選上一頁的先後順序,就可能因為快取而受影響。單元測試則是在使用者無法登入時,可以知道是否是哪個元件是導致錯誤的核心 - 時間上也有差異,可參考以下測試金三角,最上面的E2E test 時間和人力成本最高,size 推模也是最大 **![](https://lh3.googleusercontent.com/QNMQLW49-_2abl3QzHdO48w9ESMy8SlZ2l92tbHvJmvHoIzkoEdcHTkrdtimN273q8X062k7B4tsUp78ogimyAqp9vYKWEc1vE-xLjw-1DgpAZguTNZ5DqUDflP9uiVmzfJa8axvqdNCMGerNFgP2g5AHQ=s2048)** ## Matchers matchers 確保結果會如我們所預期 make sure a value will be what we expected ### cheatsheet - Jest [https://jestjs.io/docs/expect](https://jestjs.io/docs/expect) - @testing-library/jest-dom https://github.com/testing-library/jest-dom 全部講有點多,有需要時再去查找 ### Custom Matchers 在接下來的例子中雖然只有兩行變一行的差距,但在龐大的project中,可能會需要重複寫無數次,custom matchers 可以簡化流程,透過命名增加可讀性 預期結果:form裡面有2個button 1. screen.getByRole 查找form 2. within(form).getAllByRole 指定在form範圍中 取得所有button 3. expect('buttons').toHaveLength(2) 預期有2個button ```i! // Before: 原本寫法,分別抓取 form、button,再透過 expect 比對結果 const form=screen.getByRole('form') const buttons=within(form).getAllByRole('button'); expect('buttons').toHaveLength(2); // After: 建立 toContainRole 這個 custom matchers,簡化其中的抓取元素的步驟,讓 toContainRole 可以被更廣泛運用 // 之後如果有同樣「想要在A中找出有?個B」的測試邏輯的話,就可以再拿來用 const form=screen.getByRole('form') expect(form).toContainRole('button',2); ``` ### 建立 custom matchers ```javascript function toContainRole(container, role, quantity=1){ const elements=within(container).queryAllByRole(role) if(elements.length==quantity){ return { pass:true, } } return { pass:false, message: ()=> `Expected to find ${quantity} ${role} elements. Found ${elements.length} instead` } } expect.extend({toContainRole}) ``` - 第一個 argument 會是傳進 expect 中的參數,`expect(form).toContainRole('button',2);` 如左例子,form 就會是第一個參數 - 預期可以計算 form 裏面 button 的數量,所以第二參數帶 role,指定 conatiner 元素底下的 role,quantity 數量預設會有一個 - matchers 結果必須回傳 object - pass:true:object 中 pass 代表是否通過,如果 true 則是通過,如果false 則是不通過 - message:pass:false 不通過的話,可帶入 message,不通過測試時會顯示的訊息,需帶入 function ,並回傳字串 - expect.extend({toContainRole}):在expect中建立custom matchers,custom matchers function 需要包裹 object queryByRole 是確認元素是否存在,如果沒有會回傳null,不會error,在這裡會用這個是為了讓function可以順利run出指定pass:false不通過訊息 getByRole 如果沒有會直接error ## React Router in Testing 測試最麻煩要特別處理的三部分 mock module navigation(本次主題) act function ### React router 外部套件處理 測試環境的元件有用 router Link 的話,會發生錯誤 ` Error: Uncaught [Error: useHref() may be used only in the context of a <Router> component.] ` link 是 react-router-dom 裡的其中一個元件,來自於外部套件,但測試環境需要特別處理套件使用 當我們使用 link,要連結到另外一個元件或頁面時,他會去 top level 的結構中找到 react router context,例如 /list 會去找相對應 table 元件 但 link 的 context 並不在,因為我們進行測試時,只有建立了我們需要測試的元件,而沒有建立 context ![](https://hackmd.io/_uploads/ryQhXmtBh.jpg) ### Solution 在測試環境中,需要在乎的只有測試元件,根本不在意要連結到哪個頁面,但還是要在要測試的元件外包裹 routers 有 2 個 router 選項可以滿足 link 讓他可以作用 (react router testing [https://v5.reactrouter.com/web/guides/testing](https://v5.reactrouter.com/web/guides/testing)) - BrowserRouter:將URL儲存在address bar - MemoryRouter:將URL存在記憶體中(recommend) ![](https://hackmd.io/_uploads/Syk6m7FHh.png) ### demo 不知道 router 要特別處理的情況,寫出來的測試 會遇到 error ` Error: Uncaught [Error: useHref() may be used only in the context of a <Router> component.] ` ```javascript import RepositoriesListItem from './RepositoriesListItem'; import { render, screen } from '@testing-library/react'; function renderComponent() { const repository = { full_name: 'facebook/react', language: 'react', description: 'big app', owner: 'facebook', name: 'react', html_url: 'https://github.com/facebook/react', }; render( <RepositoriesListItem repository={repository} /> ); } test('render repositeries item with html link', () => { renderComponent(); }); ``` 為了解決 Router 出現的問題,引入 MemoryRouter,包裹在 RepositoriesListItem 測試元件外,讓 link 可以查找到 router context ```javascript import RepositoriesListItem from './RepositoriesListItem'; import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; // import MemoryRouter function renderComponent() { const repository = { full_name: 'facebook/react', language: 'react', description: 'big app', owner: 'facebook', name: 'react', html_url: 'https://github.com/facebook/react', }; render( // 為了滿足 link 的作用條件 <MemoryRouter> <RepositoriesListItem repository={repository} /> </MemoryRouter> ); } test('render repositeries item with html link', () => { renderComponent(); }); ```