测试React组件
在前端开发中,组件是一个重要的模块,一个组件拥有某个完整的功能,能够对我们的代码进行最大程度的复用。因此在进行单元测试的时候,往往也需要对重要的组件进行测试。
Testing Library
Testing Library是一个专门用来做测试的工具库,该库提供了一系列的API和工具,可以用来测试Web组件。
Jest是一个完整的测试框架,提供了诸如匹配器、Mock库、断言之类的工具,设计目标是提供一个完整的测试工具链,测试的重点在某个函数的功能是否完整。而Testing library是一个测试工具库,其设计理念是测试组件的行为而不是实现细节,通过这提供的一些API可以模拟浏览器中与应用交互的方式。Testing library是一个通用库,可以和各种框架进行结合。
在进行React组件测试时,Jest和Testing library一般都是配合着使用:
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
在Testing library库里面,有很多的扩展库,如@testing-library/jest-dom、@testing-library/react、@testing-library/user-event:
@testing-library/react:Testing library的核心库,提供了一组用于测试React组件的工具,例如render、screen、fireEvent等。它可以帮助你在测试中查询和操作组件中的DOM元素,以及模拟用户行为,例如点击、输入等。@testing-library/jest-dom:Jest的一个扩展库,提供了一组Jest断言方法,用于测试DOM元素的状态和行为。它可以帮助你编写更简洁、更可读的测试代码,例如toBeInTheDocument、toHaveTextContent等。@testing-library/user-event:这个库提供了一组用于模拟用户行为的工具,例如type、click、tab等。它可以帮助你编写更接近真实用户体验的测试,例如模拟用户输入、键盘操作等。
render方法
该方法接收一个React组件作为参数,将其渲染为DOM元素,并返回一个对象,对象身上包含一些重要的属性如下:
container:渲染后的DOM元素,可以通过操作它来模拟用户行为,或者进行其他的断言验证。baseElement:整个文档的根元素<html>。asFragment:将渲染后的DOM元素转换为DocumentFragment对象,方便进行快照测试。debug:在控制台输出渲染后的DOM元素的HTML结构,方便调试。
screen对象
该对象封装了一个常用的DOM查询和操作的函数,screen也提供了一些常用的方法:
screen.getByLabelText:根据<label>元素的for属性或者内部文本,获取与之关联的表单元素。screen.getByText:根据文本内容获取元素。screen.getByRole:根据role属性获取元素。screen.getByPlaceholderText:根据placeholder属性获取表单元素。screen.getByTestId:根据data-testid属性获取元素。screen.queryBy:类似的,还有一系列queryBy函数,用于获取不存在的元素时不会抛出异常,而是返回null。
基本测试
- HideMessage.tsx
- HideMessage.spec.tsx
import { useState, ReactNode } from 'react'
interface Props {
children: ReactNode
}
function HiddenMessage({ children }) {
const [isShow, setIsShow] = useState(false)
return (
<div>
<label htmlFor='toggle'>Show Info</label>
<input
type='checkbox'
name='toggle'
id='toggle'
checked={isShow}
onChange={(e) =>
setIsShow(e.target.checked)
}
/>
{isShow ? children : null}
</div>
)
}
export default HiddenMessage
import {
render,
screen,
fireEvent
} from '@testing-library/react'
import HiddenMessage from '../components/HiddenMessage'
test('Hide Message', () => {
const testMessage = 'This is a testing message'
render(<HiddenMessage>{testMessage}</HiddenMessage>)
expect(screen.queryByText(testMessage)).toBeNull()
fireEvent.click(screen.getByLabelText('Show Info'))
expect(
screen.getByText(testMessage)
).toBeInTheDocument()
})
React Hooks
在进行React开发时,Hook是一个非常重要的功能模块,自定义Hook作为一块公共逻辑的抽离,也会像组件一样被用到多个地方,因此对Hook的测试也是非常有必要的。
Hook没有办法像普通函数一样直接进行测试,因为在 React 中规中,Hook 必须要在组件里面使用,否则会报错。
在Testing library里面提供了一个@testing-library/react-hooks的扩展库,专门用于测试React Hooks,但从React 18开始,相关功能已经集成到了@testing-library/react,因此无需安装额外的库。
- useCounter.ts
- useCounter.spec.tsx
import { useState } from 'react'
interface Options {
min?: number
max?: number
}
type ValueParam = number | ((c: number) => number)
function getTargetValue(
val: number,
options: Options = {}
) {
const { min, max } = options
let target = val
if (typeof max === 'number') {
target = Math.min(max, target)
}
if (typeof min === 'number') {
target = Math.max(min, target)
}
return target
}
function useCounter(
initialValue = 0,
options: Options = {}
) {
const { min, max } = options
const [current, setCurrent] = useState(() => {
return getTargetValue(initialValue, {
min,
max
})
})
const setValue = (value: ValueParam) => {
setCurrent((c) => {
const target =
typeof value === 'number' ? value : value(c)
return getTargetValue(target, {
max,
min
})
})
}
const inc = (delta = 1) => {
setValue((c) => c + delta)
}
const dec = (delta = 1) => {
setValue((c) => c - delta)
}
const set = (value: ValueParam) => {
setValue(value)
}
const reset = () => {
setValue(initialValue)
}
return [
current,
{
inc,
dec,
set,
reset
}
] as const
}
export default useCounter
import useCounter from '../hooks/useCounter'
import { renderHook, act } from '@testing-library/react'
test('tesing inc', () => {
const { result } = renderHook(() => useCounter(0))
act(() => result.current[1].inc(2))
expect(result.current[0]).toEqual(2)
})
test('tesing dec', () => {
const { result } = renderHook(() => useCounter(0))
act(() => result.current[1].dec(2))
expect(result.current[0]).toEqual(-2)
})
test('set value', () => {
const { result } = renderHook(() => useCounter(0))
act(() => result.current[1].set(100))
expect(result.current[0]).toEqual(100)
})
test('set max value', () => {
const { result } = renderHook(() =>
useCounter(0, { max: 100 })
)
act(() => result.current[1].set(1000))
expect(result.current[0]).toEqual(100)
})
test('set min value', () => {
const { result } = renderHook(() =>
useCounter(0, { min: -100 })
)
act(() => result.current[1].set(-1000))
expect(result.current[0]).toEqual(-100)
})
renderHook会渲染一个测试组件,在组件中可以使用自定义hook,从renderHook的返回值中可以解构出自定义hook中返回的状态值以及修改状态值的方法。- 使用
act方法,该方法主要是用来模拟React组件的交互行为,并且触发更新。 - 最后进行
expect断言。
异步Hook
有些时候Hook会包含一些异步的代码,例如计时器:
const asyncInc = (delta = 1) => {
setTimeout(()=>{
setValue((c) => c + delta);
}, 2000);
};
test("asyns inc",async ()=>{
jest.useFakeTimers();
const {result} = renderHook(()=>useCounter(0));
act(() => result.current[1].asyncInc(2));
expect(result.current[0]).toEqual(0);
await act(()=>jest.advanceTimersByTime(2000));
expect(result.current[0]).toEqual(2);
jest.useRealTimers();
});