適合閱讀者:已經會寫React,但過去完全沒寫過「Component Unit Test」的人,本篇會透過React Testing Library 提供一些概略的介紹。希望閱讀完之後,更可以理解官方文件。
Unit test 是什麼? Unit test是「模組測試」,他可以針對程式的最小單元進行測試。所謂最小單元可能是某個程式、函式、組件等等。那在這個範例中,會用來測React的component。
React Testing Libray是什麼? 是一個可以方便的 React component 測試庫,特色是可以針對 DOM 的渲染結果進行測試,而不用去在意 React component 本身語法怎麼寫。
如果想要看範例檔案,請參考連結 當中的 button.js
和 button.test.js
一個好的按鈕,通常表示「當點擊該按鈕被時,預期他會執行該做的事情(例如:執行函式)」
當這裡的TodoButton被點擊時,我們會希望handleButtonClick()是真的有被呼叫到的。(在這裡它以onButtonClick的名字,被傳遞到了TodoButton裡面進去)
// App.jsimport TodoButton from 'TodoButton';return (<TodoButtononButtonClick={handleButtonClick}/>)
如果以元件本身來看,當TodoButton被點擊的時候,我們希望onButtonClick真的被呼叫
// TodoButton.jsfunction TodoButton({ onButtonClick }){return (<MyButtononClick={onButtonClick}>Add todo</MyButton>)}export default TodoButton;
.test.js
├── App.js└── Component└── TodoButton.js└── TodoButton.test.js // 新增TodoButton的測試檔案
每一個元件都應該要有測試,因此App.js也應該有個App.test.js的測試,但這邊先寫TodoButton的。
// TodoButton.test.jsimport 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 渲染出來,因為之後才可以被抓得到<TodoButtononButtonClick={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-library
、jest-dom/extend-expect
、userEvent
和 TodoButton
引用進來
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 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 3.156s
render(<TodoButton >)
// 假設,最後會渲染成:<button> Add todo </button>const todoButton = screen.getByText('Add todo')
而不是像這樣
const todoButton = screen.getByComponentName('TodoButoon')// 因為再渲染之後,並不會存在 <TodoButoon> (雖然也沒有 getByComponentName 這種選擇器
userEvent.click(todoButton);
expect(mockFunc.mock.calls.length).toBe(1);
對於究竟要使用哪個選擇器,可以參考Which query should I use?
getByText:透過文字去抓取元素
<MyButtononClick={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事件
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)
在擁有以上基礎概念之後,你可能還是有很多疑問,但你可以開始去閱讀以下文件了
Quick Links
Legal Stuff
Social Media