# [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 推模也是最大
****
## 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

### 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)

### 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();
});
```