Skip to main content

[note] React Testing

$ npm test # 執行測試
$ npm test -- --coverage # 檢視測試覆蓋率

前端測試(Testing)的概念#

前端測試可以分成單元測試(unit testing)、整合測試(integration testing)和 end-to-end testing。詳細的說明可參考「前端測試」的內容。

前置作業#

安裝#

  • 預設的情況下,透過 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.jsbar.spec.js
  • 在名為 __tests__ 資料夾內的所有 .js.jsx.ts.tsx 都會被執行測試

建議把執行測試的檔案和原本的檔案放在同一資料夾中,如此可以簡化 import 的路徑。舉例來說,把 App.jsApp.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"
}

單元測試(Unit Testing)#

先從最基本的 Unit Test 開始。Unit Test 只需要使用 Jest 提供的方法即可完成,測試的起手式多半都是長這樣。使用 test 把要測試的內容包起來,Jest 則提供各種 method 可以幫你比對 expect 和 actual 之間是否相同:

基本使用#

要建立測試,只需使用 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);
});
});

常用方法#

  • 判斷是否相同
    • expect.toBe(value):使用 Object.is 來比對,不適合物件、陣列、浮點數
    • toEqual():比對物件內容是否相同
    • toBeCloseTo():比對浮點數
  • 判斷是否存在或真假
    • toBeTruthy(), toBeFalsy()
    • toBeUndefined()

元件測試/整合測試(Integration Testing)#

react-testing-library#

這裡會使用目前相當 Popular 用來測試 React Component 的 react-testing-library,並且一般會搭配 jest-dom 使用。

react-testing-library 其實是基於 Testing Library 的一套工具,方便 React 的開發者去針對 DOM、使用者的操作行為去進行測試。它提供了一些方法讓開發者可以去 Query DOM 裡面的元素,並且等待 component 重新 render 後看有無任何變化。適合用在測試一個較完整的 component 或 modules。

💡 透過 create-react-app 建立的 React 專案已經預裝好 react-testing-libraryjest-dom。

在 React Testing Library 中提供了許多方法幫助開發者去 Query DOM 的內容,再把這些內容 Query 出來後,我們一樣需要使用 Jest 提供的 API 來比較「期待的」和「實際的」是否有差異。因此,React Testing Library 只是個幫忙測試的工具,並不是實際可以進行直接執行的 test runner。

Integration Test 和 E2E Test 很大的差異在於,Integration Test 通常不會實際去呼叫 server 的 API,而是用模擬回傳資料的方式。因此會需要模擬呼叫 API 的方法(例如,axiosfetch),以及模擬 API 回傳的資料(mock data)。

note

除了 react-testing-library 外,還有一套稱做 react-hook-testing-library 工具,但這套工具主要是在測試 React Custom Hooks 的,並非針對 React Components,因此較靠近 Unit Test 一些。

前置設定#

// src/setupTests.js
// 這支檔案會在執行測試前自動被執行
import '@testing-library/jest-dom/extend-expect';

範例#

testId 的使用#

為了方便找到 DOM 元素,可以在要測試的元件上,加上 testId

<img data-testid="hero" src={images} alt="animal" />

這時候就可以透過 findByTestId 這個方法來取得對應的元素:

test('...', async () => {
const carousel = render(<Carousel images={images} />);
const hero = await carousel.findByTestId('hero');
});

render 的使用#

user-management @ pjchender private github

// src/App.test.js
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
it('renders welcome message', () => {
const { getByText } = render(<App />);
expect(getByText('Learn React')).toBeInTheDocument();
});

若有搭配非同步的操作,可以在 it 後的 callback 帶入 async function:

import { expect, test } from '@jest/globals';
import { render } from '@testing-library/react';
import Carousel from './Carousel.js';
// 可以在 test 後的 callback 使用 async,並在函式中搭配 await 使用
test('lets users click on thumbnails to make them the hero', async () => {
const images = ['0.jpg', '1.jpg', '2.jpg', '3.jpg'];
const carousel = render(<Carousel images={images} />);
const hero = await carousel.findByTestId('hero');
expect(hero.src).toContain(images[0]);
});

rerender 的使用#

import { render, waitFor, screen } from '@testing-library/react';
import myHook from '@/hooks/myHook';
describe('test some hook or utility', () => {
const MockComponent = ({ ...props }) => {
return <div {...props} />;
};
test('this myHook', async () => {
const { rerender, unmount } = render(<MockComponent />);
const errorMessage = await waitFor(() =>
screen.findByText(/Something go wrong. Please try again later./),
);
expect(errorMessage).toBeInTheDocument();
rerender(<MockComponent foo="bar" />);
const nameEmpty = screen.getByText(/Please enter your name/);
expect(nameEmpty).toBeInTheDocument();
unmount();
expect(nameEmpty).not.toBeInTheDocument();
});
});

專注和略過某些測試#

只需要把 it() 改成 xit() 就可以暫時略過某些測試項目;fit() 則可以讓你專注於某個測試項目而不用去執行其他的測試。

測試覆蓋率報告(Coverage Reporting)#

執行 npm test -- --coverage 就可以取得測試覆蓋率的報告。這個部分有許多設定可以自行修改,可以參考 configuration 的部分。

快照測試(Snapshot Testing)#

Jest 提供了一個快照測試(snapshot testing),它會自動將元件產生一個文字版的快照,並儲存在硬碟上,每當 UI 改變時,就會收到通知,此時你可以檢視這個變更是不是預期的改變,是的話可以執行 jest -u,否則的話就去修復這個 bug。

在下面的範例中,react-test-rendererreact-test-renderer/shallow 的差別在於:

  • react-test-renderer 會一直渲染到最底層的 component
  • react-test-renderer/shallow 只會渲染該 component 第一層

範例#

使用 react-test-renderer#

keywords: react-test-renderer, toMatchSnapshot()

test-renderer @ ReactJS

建立 snapshot testing 的方式如下:

  • 第一次執行程式時,Jest 會自動產生 snapshot 並保存起來
  • 當 UI 有變更的時候,Snapshot 就會顯示異動的部分
  • 若確定這個異動的部分是合理且正確的,則執行 npm run test -- -u 表示現在的 Snapshot 是最新的,要覆蓋原本就的,產生新的 Snapshot。
import { create } from 'react-test-renderer';
import Button from './Button';
test('snapshot of button', () => {
const tree = create(<Button />).toJSON();
expect(tree).toMatchSanpshot();
});

使用 shallow renderer#

keywords: createRenderer#

shallow renderer @ React

import { createRenderer } from 'react-test-renderer/shallow';
test('renders correctly with some pets', () => {
const r = createRenderer();
r.render(<Results pets={pets} />);
expect(r.getRenderOutput()).toMatchSnapshot();
});

其他問題#

無法使用 __mocks__ 資料夾#

透過 create-react-app 建立的 React 專案目前(2020-01-22)可能會吃不到 __mocks__ 資料夾中的設定,解決方法是把 __mocks__ 的資料夾搬到 src 裡面。

參考資料:

參考資料#

Last updated on