給初學者的前端 Unit test 教學—— 以 React Testing Library 為例

Published in Web development
August 26, 2020
1 min read

給初學者的前端 Unit Test 教學(以React Testing Library 為例)

適合閱讀者:已經會寫React,但過去完全沒寫過「Component Unit Test」的人,本篇會透過React Testing Library 提供一些概略的介紹。希望閱讀完之後,更可以理解官方文件。

  • Unit test 是什麼? Unit test是「模組測試」,他可以針對程式的最小單元進行測試。所謂最小單元可能是某個程式、函式、組件等等。那在這個範例中,會用來測React的component。

  • React Testing Libray是什麼? 是一個可以方便的 React component 測試庫,特色是可以針對 DOM 的渲染結果進行測試,而不用去在意 React component 本身語法怎麼寫。

以一個按鈕為例,入門第一個 Unit Test測試

如果想要看範例檔案,請參考連結 當中的 button.jsbutton.test.js

怎樣算是一個正常的按鈕呢?

一個好的按鈕,通常表示「當點擊該按鈕被時,預期他會執行該做的事情(例如:執行函式)」

以外層來看(使用按鈕的那層)

當這裡的TodoButton被點擊時,我們會希望handleButtonClick()是真的有被呼叫到的。(在這裡它以onButtonClick的名字,被傳遞到了TodoButton裡面進去)

// App.js
import TodoButton from 'TodoButton';
return (
<TodoButton
onButtonClick={handleButtonClick}
/>
)

以元件本身來看(按鈕本身)

如果以元件本身來看,當TodoButton被點擊的時候,我們希望onButtonClick真的被呼叫

// TodoButton.js
function TodoButton({ onButtonClick }){
return (
<MyButton
onClick={onButtonClick}>Add todo</MyButton>
)
}
export default TodoButton;

如何幫Button寫測試?

首先,在元件本身那層(TodoButton)開.test.js

├── App.js
└── Component
└── TodoButton.js
└── TodoButton.test.js // 新增TodoButton的測試檔案

每一個元件都應該要有測試,因此App.js也應該有個App.test.js的測試,但這邊先寫TodoButton的。

開始撰寫測試

// TodoButton.test.js
import React from 'react'
import { render, screen } from '@testing-library/react'// 把 testing-library 常用的值引用進來
import '@testing-library/jest-dom/extend-expect' // 將檢測用的 expect 函式 引用進來(後面會看到)
import userEvent from '@testing-library/user-event';
import TodoButton from './TodoButton'; // 把 TodoButton 引用進來
describe('測試Button是否正常', () => {
test('當按下按鈕時,確定 mockFunc 會被呼叫', () => {
const mockFunc = jest.fn(); // 宣告一個模擬用的函式
render( // 將 TodoButton 渲染出來,因為之後才可以被抓得到
<TodoButton
onButtonClick={mockFunc}
/>
);
const todoButton = screen.getByText('Add todo') // 在這邊「 Add todo 」是 TodoButton 裡面的內容
userEvent.click(todoButton); // 模擬使用者的點擊行為
expect(mockFn).toBeCalledTimes(1); // 去預測函式是否真的因為點擊而被呼叫
userEvent.click(todoButton);
expect(mockFn).toBeCalledTimes(2);
});
});

首先要先將 testing-libraryjest-dom/extend-expectuserEventTodoButton 引用進來

  • jest-dom/extend-expect 是用來提供檢測用的 expect 函式(不懂的話可以直接看範例)
  • userEvent是用來模擬的使用者行為

解釋程式碼

  • const mockFunc = jest.fn() :宣告一個模擬的函式,之後會測試它是否有被呼叫
  • render() :將組建渲染出來,這樣後續才可以開始測試
  • screen.getByText('XXXX') 透過「文字」來獲取元素

注意:寫測試時,我們會希望測試是穩定性的,不會因為改動組件的一些內容,就需要導致測試重寫。因此在抓取元素的時候,會以終為始的去思考,直接思考「渲染後的結果」是什麼而去抓取,而不是透過getByComponent('TodoButton')之類的方式去抓取(雖然也沒有這個方法)。比如說,會從他已經被渲染成<button> Add todo</button> 的這個狀態去思考如何抓取,而不是嘗試去抓 <TodoButoon>

  • screen.debug() 如果想知道自己抓到了什麼,可以把它印出來
screen.debug(todoButton) //會將todoButton印出
  • userEvent.click() 模擬使用者點擊的行為
  • expect(mockFn).toBeCalledTimes(1) 預期mockFunc函式再點擊後會被呼叫一次

開始執行測試

npm run test

如果成功,就會得到結果

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.156s

複習:撰寫一個測試的步驟

  1. 首先,要定義一下應該要測試什麼? 例如「一個正常的按鈕」,表示「當按鈕被點擊時,他應該觸發相關函式」
  2. 將組建引入,並且渲染組建:要先渲染出來,才能方便在後面抓取
render(<TodoButton >)
  1. 利用選擇器抓取元素(要以渲染結果的樣子去思考),因此應該是
// 假設,最後會渲染成:<button> Add todo </button>
const todoButton = screen.getByText('Add todo')

而不是像這樣

const todoButton = screen.getByComponentName('TodoButoon')
// 因為再渲染之後,並不會存在 <TodoButoon> (雖然也沒有 getByComponentName 這種選擇器
  1. 模擬使用者的動作,例如:點擊事件
userEvent.click(todoButton);
  1. 測試一下預期的結果是否發生
expect(mockFunc.mock.calls.length).toBe(1);

常用語法

常用的選擇器

對於究竟要使用哪個選擇器,可以參考Which query should I use?

  • getByText:透過文字去抓取元素

    <MyButton
    onClick={onButtonClick}>Add todo</MyButton>
    )
    const todoButton = screen.getByText('Add todo')
  • getByLabelText:透過aria-label的屬性去抓取元素

    <input aria-label="username" />
    const inputNode = screen.getByLabelText('Username')
  • getByDisplayValue:透過value的屬性抓取元素

    <input type="text" id="lastName" value="Norris" />
    const lastNameInput = screen.getByDisplayValue('Norris')
  • getBytitle:透過title屬性抓取元素

    <span title="Delete" id="2"></span>
    <svg>
    <title>Close</title>
    <g><path /></g>
    </svg>
    const deleteElement = screen.getByTitle('Delete')
    const closeElement = screen.getByTitle('Close')

常用的模擬行為

userEvent可以模擬使用者行為,可以參考User-Event事件

  • click(element,eventInit,options):模擬點擊事件
  • dbClick(element,eventInit,options):模擬雙擊事件
  • type(element,text,[options]):模擬打字事件
  • hover(element):模擬hover事件
  • paste(element,text,eventInit,options):模擬貼上事件

常用的測試函式

  • expect(mockFn).toBeCalledTimes(): 測試某函式被呼叫的次數

    expect(mockFn).toBeCalledTimes(1)
  • expect(mockFn).toBeCalledWith(參數):測試函式傳進去的「參數」是不是特定的值

    // 預期 mockFn 會被傳入一個物件的參數
    expect(mockFn).toBeCalledWith({
    icon: "warning",
    title: "請選擇檔案",
    });
  • expect().toHaveValue():測試某個元素是否有X值

    expect(screen.getByRole('textbox')).toHaveValue('Hello,\nWorld!')
  • expect().toHaveAttribute()

    expect(screen.getByLabelText('Check')).toHaveAttribute('checked', true)

開始學習測試!

在擁有以上基礎概念之後,你可能還是有很多疑問,但你可以開始去閱讀以下文件了


Share


0 Comments
© 2025, All Rights Reserved.

Social Media