[note] React Testing
- Running Tests @ create-react-app
- react-testing-library @ testing-library
- 👍 React Testing Library Tutorial @ Youtube
- Day27 建立 Mock Module / Function 的方式(ft. TypeScript)
- Day28 測試依賴外層 Context Provider 的 React 元件:客製化 render 函式
- Day29 React Testing Library 的一些實用的小技巧
TL;DR
- Custom Jest Matchers | Jest DOM: extends Jest matchers
- Queries | Dom Testing Library: methods provided by testing library
- Cheatsheet | React Testing Library
- Convenience APIs | User Event
$ npm test # 執行測試
$ npm test -- --coverage # 檢視測試覆蓋率
// 專注或略過某些測試
// https://pjchender.dev/npm/npm-jest/#專注和略過某些測試
test.only(() => {
/* ... */
});
test.skip(() => {
/* ... */
});
Snippets
// https://www.udemy.com/course/learn-react-query/learn/lecture/26581756
import { fireEvent, screen, waitForElementToBeRemoved } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { mockUser } from '../../../mocks/mockData';
import { render } from '../../../test-utils';
import { Calendar } from '../Calendar';
// mocking useUser hooks
jest.mock('../../user/hooks/useUser', () => ({
__esModule: true,
useUser: () => ({ user: mockUser }),
}));
test('Cancel appointment', async () => {
render(
<MemoryRouter>
<Calendar />
</MemoryRouter>,
);
const cancelButtons = await screen.findAllByRole('button', {
name: /cancel appointment/i,
});
fireEvent.click(cancelButtons[0]);
const alertToast = await screen.findByRole('alert');
expect(alertToast).toHaveTextContent('Appointment cancelled');
const alertCloseButton = screen.getByRole('button', { name: 'Close' });
fireEvent.click(alertCloseButton);
await waitForElementToBeRemoved(alertToast);
});
認識相關套件
- jsdom:一般也指測試的執行環境(environment),目的是模擬瀏覽器的行為、API,讓開發者能在 Node.js 的環境下模擬瀏覽器的操作。其他這類的套件如 happy-dom。
- Jest:是一個用來執行 JavaScript 測試的框架(JavaScript Test Framework),又稱 test runner,它讓開發者能夠執行測試、撰寫斷言,提供的 API 像是
expect
、describe
、test
、toBe
、toEqual
等等。其他這類的 JS testing frameworks 如 Vitest、mochajs、Jasmine 等等 @testing-library/dom
:又稱作「DOM Testing Library」、「Web Testing Library」,是 Testing Library 的核心,一般如果講 Testing Library 就是在講這個。它讓開發者可以使用各種 queries 來找到 DOM Node(例如,getBy
、findBy
),使用 user-event(fireEvent)來觸發事件,其他與 Testing Library 相關的套件(例如,@testing-library/react
),多基於這個來添加功能。@testing-library/jest-dom
:原本 Jest 就有提供許多不同的 matchers(例如,toBe()
、toEqual()等等),
@testing-library/jest-dom則是擴充了更多可以在 Jest 中使用的 matchers,讓開發者可以使用
toBeInTheDocument()` 等這類和 DOM 有關的 matchers。@testing-library/react
:基於@testing-library/dom
,它讓開發者可以把 React 元件 render 到 DOM 上,像是render
、screen
、rerender
、unmount
等等。不需要搭配 Jest 才能使用。其他這類的工 具如 Enzyme。@testing-library/user-event
:模擬使用者的操作來測試使用者與 UI 的互動。相較於@testing-library/dom
中的fireEvent
更能模擬使用者的行為。
需要特別留意 @testing-library/dom
和 @testing-library/jest-dom
是兩個功能不同的套件。@testing-library/dom
是 Testing Library 的核心,@testing-library/react
即是基於它下去開發。@testing-library/jest-dom
則是替 Jest 提供了更多的 Matchers 可以使用,也就是說 @testing-library/jest-dom
是基於 Jest 的 Matchers。
ESLint Plugin
如果有使用 @testing-library
的話,也可以搭配使用他們提供的 ESLint plugin,例如 eslint-plugin-testing-library
和 eslint-plugin-jest-dom:
// .eslintrc.json
{
"overrides": [
{
"files": ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"],
"extends": ["plugin:testing-library/react", "plugin:jest-dom/recommended"]
}
]
}
前端測試(Testing)的概念
前端測試可以分成單元測試(unit testing)、整合測試(integration testing)和 end-to-end testing。詳細的說明可參考「前端測試的概念與類型」的內容。
單元測試(Unit Testing):以 Jest 為例
先從最基本的 Unit Test 開始。Unit Test 只需要使用 Jest 提供的方法即可完成,測試的起手式多半都是長這樣。使用 test
把要測試的內容包起來,Jest 則提供各種 method 可以幫你比對 expect 和 actual 之間是否相同。
什麼時候該執行單元測試
- 當這個 function 的邏輯複雜到無法透過 integration tests 來測得
- 當這個 function 有太多的 edge cases,使得 integration tests 無法 cover 到各種情況時
安裝
-
預設的情況下,透過 Create React App 所建立的專案都已經把測試所需要的套件和環境設定好了,除了 Jest 之外,也安裝好了 react testing library 可以直接使用。因此只需要在終端機輸入
npm test
即可。 -
若是自己建立的專案,需要透過 babel 來處理
// .babelrc
// 參考設定
{
"presets": [
[
"@babel/preset-env",
{
// babel 會打包成相容於 node 12 的程式碼
"targets": {
"node": 12
}
}
],
"@babel/preset-typescript"
]
}
檔名慣例(Filename Conventions)
執行 npm test
時,Jest 預設就會去找符合下述規則的檔案進行測試:
- 檔案名稱中包含
.test
或.spec
的檔案,例如,foo.test.js
、bar.spec.js
- 在名為
__tests__
資料夾內的所有.js
、.jsx
、.ts
和.tsx
都會被執行測試
建議把執行測試的檔案和原本的檔案放在同一資料夾中,如此可以簡化 import
的路徑。舉例來說,把 App.js
和 App.test.js
放在同一個資料夾中,如此在執行測試時只需要寫 import App from './App'
即可。
Jest CLI
當執行 npm test
時,Jest 會自動進入「監控模式(watch mode)」,每當你儲存檔案它都會自動重新執行測試。這個監控器(watcher)包含一個 CLI 可以讓你選擇要執行所有的測試,還是只執行符合條件下的檔案來測試。
$ npm test
$ jest --watch
$ jest --watchAll=false # 不要監控
預設的情況下,當你執行 npm test
之後,Jest 只會針對上次 commit 後有變更的檔案進行測試,同時這也假設了你不會經常把沒有通過測試的檔案 commit 起來。
在 CLI 中你可以點擊 a
來強制 Jest 執行所有的測試。
Jest 在 continuous integration server 上、並非 Git 或 Mercurial 資料夾的專案總是會執行全部的測試。
測試環境設定與初始化
在測試中可能會使用到瀏覽器的 API 或進行全域環境的設定,這時候可以在專案中加入 src/setupTests.js
這支檔案,這支檔案會在執行測試前自動被執行。
舉例來說:
// src/setupTests.js
// 這支檔案會在執行測試前自動被執行
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
CRA 若曾經 eject 過
若你在建立 src/setupTests.js
前已經把 create-react-app
所建立的專案執行 eject
,那麼 package.json
檔案將不會包含任何參照到 setupTests.js
的資訊,因此應該要在手動添加設定:
// package.json
"jest": {
// ...
"setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.js"
}
基本使用
要建立測試,只需使用 it()
或 test()
函式,並在參數中放入該測試的名稱和程式碼。在 Jest 中提供內建的 expect()
函式來建立斷言(assertions),基本的測試會長得像這樣:
import sum from './sum';
it('sums numbers', () => {
expect(sum(1, 2)).toEqual(3);
expect(sum(2, 2)).toEqual(4);
});
// toBe 使用 Object.is 來比對,若想要比對物件內容是否一樣需使用 toEqual
test('two plus two is four', () => {
const actualValue = 2 + 2;
const expectedValue = 4;
expect(actualValue).toBe(expectedValue);
});
每一個 test
裡面會包起來的測試案例稱作 test case;當我們有很多相關連的 test case 想要放在一起跑或做某些相同的前處理時,可以把這些 test cases 包在 test suite 中。
在 Jest 中可以使用 describe
來建立 test suite:
describe('Test all math functions', () => {
beforeAll(() => {
/* ... */
});
test('two plus two is four', () => {
const actualValue = 2 + 2;
const expectedValue = 4;
expect(actualValue).toBe(expectedValue);
});
test('two multiplied by two', () => {
const actualValue = 2 * 2;
const expectedValue = 4;
expect(actualValue).toEqual(expectedValue);
});
});
在 Jest 中提供了 expect
這個來建立斷言(assertion),但斷言與實際結果不合時,就會拋出錯誤(throw errors)。實際上,Jest 就是看 test case 中有沒有 throw errors 來判斷測試是否失敗,也就是說,如果你想讓測試失敗,在測試中使用 throw Error
亦可達到一樣的效果;如果整個 test case 執行後都沒有 error,則表示測試成功。
常用方法
- Expect @ Jest Docs API
- Using Matcher @ pjchender.dev
- 判斷是否相同
expect.toBe(value)
:使用 Object.is 來比對,不適合物件、陣列、浮點數toEqual()
:比對物件內容是否相同toBeCloseTo()
:比對浮點數
- 判斷是否存在或真假
toBeTruthy()
,toBeFalsy()
toBeUndefined()
擴充 Jest 的 Matchers(由 @testing-library/jest-dom 提供)
可以透過 TypeScript 的型別檔檢視 @testing-library/jest-dom
提供的所有 matchers:
// node_modules/@types/testing-library__jest-dom/matchers.d.ts
export interface TestingLibraryMatchers<E, R> extends Record<string, any> {
toBeInTheDocument(): R;
toBeVisible(): R;
// ...
}
Setup React Testing Library with Vitest @ PJCHENder
DOM Element
expect(<element>).toContainElement(<otherElement>)
expect(<element>).toBeInTheDocument()
expect(<element>).toBeVisible()
:使用者真的可以看到該元素,例如opacity: 0
的話就會 Fail。