React 搭配 Testing Library 實作測試
本篇使用的 JavaScript 框架是 Jest,所以在開始前要先了解一些 Jest 基本語法,才會比較快理解喔!
Testing Libary
雖然 Jest 本身有提供很多測試方法,但在測試上都比較偏向邏輯測試,像是 a + b 是否等於 c。而實際上我們所需要的測試有一大部分也包含 UI 的測試,像是畫面上有沒顯示正確文字,或是使用者點擊有沒有跳出視窗等等,這時候就是 Testing Library 出馬的時候了!
它的官網介紹就很明確地告訴你
The @testing-library family of packages helps you test UI components in a user-centric way.
也就是 @testing-library 系列的套件可以幫助你以用戶為中心的方式測試 UI 元件。
Testing Library 不只提供可以共通使用的測試套件,還有專為各個框架所出的套件,基本的三大框架 React、Vue、Angular 都有屬於他們的測試 UI 套件。而比較新的框架像是 Preact、Svelte 也都有在持續更新。
Testing Libaray For React
以 React 來說,搭配 Jest 會需要的 Testing Libary 有以下幾個:
1. @testing-library/react - 模擬 React 渲染,Ex:render、screen
2. @testing-library/jest-dom - 擴充 jest 的斷言庫,Ex:toBeInTheDocument、toHaveClass
3. @testing-library/user-event - 模擬使用者操作,Ex:useEvent.click、userEvent.type
接下來來各別介紹這三個的使用時機:
◆ @testing-library/react
主要是提供模擬渲染 UI 畫面的函式,比較常用的有
1. render()
就如同 React 中的 render,就是模擬渲染 React 元件,假設有一個 <Home/>
元件:
export const Home = () => {
return <h1>Home Page</h1>
};
要測試元件的 DOM 元素時,就可以使用 render()
函式。
import { render } from '@testing-library/react'
describe('testing home component', () => {
it('show Home Page in the home component', () => {
const {getByText} = render(<Home/>);
expect(getByText('Home Page')).toBeTruthy(); //使用 getByText 判斷抓到文字的元素是否存在
});
});
render()
函式會回傳一個物件包含一些屬性對象:
-
...Query:getBy、queryBy、findBy、getAllBy、queryAllBy、findAllBy,取得元件內的元素。
-
container:渲染的 DOM 節點。
-
debug:偵錯函式,可以顯示當前的 DOM 結構。
-
rerender:重新渲染元件。
-
unmount:取消渲染。
Query 有很多不同的取元素方法,有 get、find、query,各自都有取得多元素的 all 方法
Query 種類\結果 |
沒有符合 |
一項符合 | 大於一個符合 | 是否為非同步函式 |
getBy... | Throw error | 回傳元素 |
Throw error | ❌ |
queryBy... | 回傳 null |
回傳元素 |
Throw error | ❌ |
findBy... | Throw error | 回傳元素 |
Throw error | ✅ |
getAllBy... | Throw error | 回傳陣列元素 |
回傳陣列元素 | ❌ |
queryAllBy... | 回傳 [] |
回傳陣列元素 |
回傳陣列元素 | ❌ |
findAllBy... | Throw error | 回傳陣列元素 |
回傳陣列元素 | ✅ |
看起來很容易搞混,不過他們都有各自使用的時機
-
getBy...:大部分都可以使用,判斷元素是否存在。
-
queryBy...:因為找不到會回傳
null
的特性,所以常用來判斷元素是否一開始不存在。 -
findBy...:非同步函式,可以判斷需等待的元素是否存在,例如 API 回傳才會顯示在畫面上。
2. screen()
雖然 render()
解決了模擬 UI 的情況,不過只能根據 render()
的內容進行測試,如果有多個 render()
函式測起來就會比較麻煩,這時候就可以使用 screen()
。
其實 screen()
算是 @testing-library/dom 所提供,而所有框架的 @testing-library 底層都有 @testing-library/dom,所以這邊才可以直接做使用。
而 screen()
所抓取的元素是 <body></body>
內的所有 DOM 元素。
import { render, screen } from '@testing-library/react'
describe('testing home component', () => {
it('show Home Page in the home component', () => {
render(<Home/>);
render(<Home/>);
expect(getAllByText('Home Page')).toBeTruthy(); //使用 getAllByText 一次判斷兩個 Home 元件元素是否存在
});
});
個人在抓元素時是比較常用 screen()
勝過於使用 render()
回傳的方法,使用起來也比較方便。
3. renderHook()
顧名思義就是去模擬客製化的 React Hook,假設今天有一個計數器的 customhook
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return { count, increment, decrement };
}
export default useCounter;
要測試這個 hook 就可以使用 renderHook()
,他可以傳入兩個參數,第一個就是要測試的函式,第二是傳入函式的參數 (非必要)。
import useCounter from './useCounter';
import { renderHook, act } from '@testing-library/react';
it("should increment and decrement the counter", () => {
const { result } = renderHook(useCounter);
expect(result.current.count).toBe(0);
act(() => {
result.current.increment(); // 呼叫 + 1 函式
});
expect(result.current.count).toBe(1);
act(() => {
result.current.decrement(); // 呼叫 -1 函式
});
expect(result.current.count).toBe(0);
});
renderHook()
會回傳一個物件,可以使用 result
屬性利用 result.current
去操控函式的回傳物件。
如果有更改 state
的操作,就需要使用 act
包起來,這樣才可以即時更新 state
的狀態。
◆ @testing-library/jest-dom
@testing-library/jest-dom 提供給 Jest 很多 DOM 元素的擴充判斷,讓我們在抓元素時,有更多的方法去測試,比較常用的像是 toBeInTheDocument
、toHaveClass
等等。
以剛剛的 <Home/>
渲染測試為例
import { render } from '@testing-library/react'
describe('testing home component', () => {
it('show Home Page in the home component', () => {
const {getByText} = render(<Home/>);
expect(getByText('Home Page')).toBeInTheDocument(); //判斷元素是否存在於 Document
});
});
或是
import { render } from '@testing-library/react'
describe('testing div', () => {
it('test div classname is hide', () => {
render(<div className='hide'>test</div>);
expect(screen.getByText('test')).toHaveClass('hide'); //判斷元素是否含有指定的 class name
});
});
◆ @testing-library/user-event
最後一個就是模擬使用著操作,user-event 常常跟另一個 @testing-library/react 的 fireEvent 拿來比較,fireEvent 就是在程式碼中會用的事件處理,像是 click 或是 change,而 user-event 的底層就是 fireEvent,不過 userEvent 能更貼合使用者的模擬情況,像是同樣是在 input 輸入文字,如果使用 fireEvent 就會使用 change 的事件。
fireEvent:
import { fireEvent } from '@testing-library/react';
fireEvent.change(inputElement, { target: { value: 'Hello, world!' } });
不過如果是用 userEvent 就會使用 type 的事件。
userEvent:
import userEvent from '@testing-library/user-event';
const user = userEvent.setup()
user.type(inputElement, 'Hello, world!');
乍看之下沒有什麼不一樣,不過 userEvent 的 type 還包含使用者點擊 input,輸入文字的 keydown、keyup 事件,比起 fireEvent 的 change,更貼近實際的使用者操作情況。
除此之外,userEvent 還有提供很多實用的方法像是:
-
dblClick:點擊兩次
-
tripleClick:點擊三次
-
type:input 輸入
-
upload:上傳
-
hover/unhover:指標移進移出
-
copy/paste:複製/貼上
詳細的可以參考 User Interactions