---
# System prepended metadata

title: '[FE Sharing: react testing matchers & 測試種類'
tags: [sharing]

---

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