React 搭配Testing Library 实作测试
本篇使用的JavaScript 框架是Jest,所以在开始前要先了解一些Jest 基本语法,才会比较快理解喔!
测试库
虽然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
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!