Skip to main content

测试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组件的工具,例如renderscreenfireEvent等。它可以帮助你在测试中查询和操作组件中的DOM元素,以及模拟用户行为,例如点击、输入等。
  • @testing-library/jest-dom:Jest的一个扩展库,提供了一组Jest断言方法,用于测试DOM元素的状态和行为。它可以帮助你编写更简洁、更可读的测试代码,例如toBeInTheDocumenttoHaveTextContent等。
  • @testing-library/user-event:这个库提供了一组用于模拟用户行为的工具,例如typeclicktab等。它可以帮助你编写更接近真实用户体验的测试,例如模拟用户输入、键盘操作等。

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

基本测试

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

React Hooks

在进行React开发时,Hook是一个非常重要的功能模块,自定义Hook作为一块公共逻辑的抽离,也会像组件一样被用到多个地方,因此对Hook的测试也是非常有必要的。

Hook没有办法像普通函数一样直接进行测试,因为在 React 中规中,Hook 必须要在组件里面使用,否则会报错。

在Testing library里面提供了一个@testing-library/react-hooks的扩展库,专门用于测试React Hooks,但从React 18开始,相关功能已经集成到了@testing-library/react,因此无需安装额外的库。

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
  1. renderHook会渲染一个测试组件,在组件中可以使用自定义hook,从renderHook的返回值中可以解构出自定义hook中返回的状态值以及修改状态值的方法。
  2. 使用act方法,该方法主要是用来模拟React组件的交互行为,并且触发更新。
  3. 最后进行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();
});