以下为本节内容,内容比较多,且都是日常工作中会用到的测试用例与常见的react组件用例,相信您会有所收获的
- 测试React
- 从一个简单的测试开始
- 运行条件
- 跑通成功
- react组件测试
- input输入框
- input输入框不使用UI框架下
- 下拉列表
- 复选框checkbox
- radioGroup
- react表单上传
- 上传部分
- 测试部分
测试React
测试之前我们先安装好模块依赖
npm i @testing-library/jest-dom -D
npm i @testing-library/react -D
npm i @testing-library/user-event -D
// 类型支持
npm i @types/jest -D
模块依赖解释,和对应的文档位置:https://testing-library.com/docs/
这三个依赖包我们会用到的功能有这么几个,这几个基本上可以解决很多测试功能的问题。
// cleanup清除,一般用于全局的afterAll中
// fireEvent操作事件,如点击等
// render渲染组件,返回一个container,可用于生成快照
// waitFor如果渲染过程中涉及异步操作,那么数据的返回就必须要使用waitFor
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
// screen == container.screen,用于获取查询方法
import { screen } from '@testing-library/dom';
// userEvent同fireEvent但高于fireEvent,我们推荐优先使用userEvent,因为他还支持
// change事件
import userEvent from '@testing-library/user-event';
从一个简单的测试开始
新建一个文件 src/component/head/head.tsx
import './head.css';
const Head = () => {
return (
<div>
<p>head</p>
<div>i am head</div>
</div>
)
}
export default Head;
新建一个文件src/component/head.css
p {
font-size: 16px;
color: pink;
}
新建测试 src/__tests__/sum.test.tsx
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
import { screen } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import Head from "~/component/head/head";
describe('first test',() =>{
test('sum func',() => {
const Com = (
<Head />
)
const container = render(Com);
expect(container).toMatchSnapshot('first test')
})
})
运行条件
- 确保环境是jsdom,因此我们需要安装jest-environment-jsdom:
npm i jest-environment-jsdom -D
testEnvironment: 'jsdom',
- head组件引入正常 vite.config.ts配置
resolve: {
alias: {
'~': path.resolve(__dirname,'src'),
'src': path.resolve(__dirname,'src')
}
},
jest.config.js配置
moduleNameMapper: {
'^~/(.*)': '<rootDir>/src/$1'
},
- 解析css文件
我们需要安装identity-obj-proxy: npm i identity-obj-proxy -D,并在jest.config.js中做好配置
moduleNameMapper: {
'\\.(css|scss|less)': 'identity-obj-proxy',
'^~/(.*)': '<rootDir>/src/$1'
},
跑通成功

并在同一级目录中,生成snapshots快照

快照图还是比较有用的,在大型项目中测试通过与否会比较两次测试的结果是否会发生变化,如果发生了变化会认为测试失败;这就意味着我们不仅要上传最新的测试用例,同时对于业务中出现的时间等任何可能改变页面内容的东西,全部mock
react组件测试
主要是测试一些表单控件,暂时都在detail这个页面测试吧。这里我们使用的输入框为mui,因此需要安装一下
npm install @mui/material @emotion/react @emotion/styled // 组件
npm install @mui/icons-material // icon
官网:https://mui.com/material-ui/getting-started/installation/
好,现在直接开撸~
input输入框
目的:渲染一个页面,这个页面包含了input输入框,我们需要给输入框添加内容。
封装一个公共的input组件,因此新建文件
src/component/common/Input.tsx
import type { TextFieldProps } from '@mui/material/TextField';
import type { ReactElement } from 'react';
import TextField from '@mui/material/TextField';
// 类型写法一
type ZLTextProps = TextFieldProps & {
startAdornment?: ReactElement,
endAdornment?: ReactElement
}
// 类型写法二
type AA<Type = object> = Type & {
startAdornment?: ReactElement,
endAdornment?: ReactElement
}
type ZLTextProps2 = AA<TextFieldProps>
const ZlInput = (props: ZLTextProps): ReactElement => {
const {
name,
label,
placeholder = 'please Input',
...rest
} = props;
return (
<TextField
id={name}
label={label}
placeholder={placeholder}
{...rest}
/>
)
}
export default ZlInput;
我们在page/detail/detail中引入这个组件,并做处理
import { useState } from 'react';
import ZlInput from 'src/component/common/Input';
import {
Grid
} from '@mui/material';
import React from 'react';
const Detail = () => {
const [username,setName] = useState('');
const [age,setAge] = useState('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleChange = (e: any , val_type: string) => {
const val = e.target.value;
switch(val_type) {
case 'username':
setName(val);
break;
case 'age':
setAge(val);
break;
default:
}
}
return (
<Grid container>
<Grid item>
<ZlInput id='username'
label='username'
variant="standard"
data-testid='username'
InputProps={{
'aria-label': 'username'
}}
value={username}
onChange={(e) => handleChange(e,'username')}
/>
</Grid>
<Grid>
<ZlInput id='age'
label='age'
variant="standard"
data-testid='age'
InputProps={{
'aria-label': 'age'
}}
value={age}
onChange={(e) => handleChange(e,'age')}
/>
</Grid>
</Grid>
)
}
export default Detail;
注意了,这里的input必须要说受控组件,否则无法change改变输入框的value值。
最后建立测试 __tests__/react/com/input.test.tsx
import { render,fireEvent,waitFor } from "@testing-library/react";
import { screen } from "@testing-library/dom";
import Detail from "src/page/detail/detail";
interface InputProps {
testId?: string,
value?: string
}
const InputField = (props: InputProps): void => {
const { testId = '' , value } = props;
// 使用getByTestId时,传入值必须要初始值
const el = screen.getByTestId(testId);
// 相当于我们找到了testId所对应的dom节点,然后根据这个节点我们继续往下面找子节点内容
const input = el.getElementsByTagName('input')[0];
fireEvent.change(input,{
target: {
value: value
}
})
}
// 两种方法获取dom节点
const getElement = (
testId?: string,
selector?: string
): HTMLElement | null => {
if ( selector ) {
return document.querySelector(selector);
} else if ( testId ) {
return screen.getByTestId(testId)
}
return null;
}
describe('test react com',() => {
it('test input',() => {
console.log('kkk')
const Com = (
<Detail />
)
const container = render(Com);
InputField({
testId: 'username',
value: 'jack'
})
InputField({
testId: 'age',
value: '12'
})
const input_username = getElement('username');
const val = input_username?.getElementsByTagName('input')[0].value;
console.log(val,'input username')
expect(input_username?.getElementsByTagName('input')[0].value).toBe('jack');
const input_age = getElement('age');
expect(input_age?.getElementsByTagName('input')[0].value).toBe('12')
expect(container).toMatchSnapshot('detail-input')
})
})
测试内容讲解
render
用于渲染组件,以便生成快照
fireEvent
用于处理事件,包括点击、聚焦、改变等
screen
获取testing-libriary的dom方法,包括三大类,查询query,获取get,和查找find
官网地址: https://testing-library.com/docs/queries/about
InputField
用于改变input框的输入值,即改变value值
getElement
用于获取dom节点
快照

input输入框不使用UI框架下
我们建立一个文件 __test__/react/com/originInput.test.tsx
import { useState } from 'react';
import { screen } from '@testing-library/dom'
import { render, waitFor } from "@testing-library/react";
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom'
const Btn = () => {
const [name,setName] = useState('jack');
const handleBtn = () => {
setName('mike');
}
const handleBtn2 = (e: any) => {
const value = e.target.value;
setName(value);
}
return (
<div>
<div>
<label htmlFor="username">username</label>
<input type="text" role='username' id='username' name='username' value={name} onChange={handleBtn2} />
</div>
<button onClick={handleBtn}>Click me</button>
</div>
)
}
describe('user event test', () => {
it('something has happen',async () => {
const container = render(<Btn />);
const input = screen.getByRole('username');
const btn = screen.getByRole('button',{ name: 'Click me'})
await userEvent.clear(input);
await waitFor( () => expect(input).toHaveValue('') )
// userEvent.click(btn);
await userEvent.type(input,'mike');
await waitFor( ()=> expect(input).toHaveValue('mike') );
expect(container).toMatchSnapshot('auth_button_mock_axios2');
})
})
解读:
- 思路和 上一个例子类似,组件是手控组件
- 这里我们事件的操作换成了userEvent,官网:https://testing-library.com/docs/user-event/intro
- 这里的type是输入值的意思
- 这里的toHaveValue匹配器并不是jest所有的,是我们引入了jest/dom:import '@testing-library/jest-dom' 才有的,大家可以去看源码,相信我会收获不少的。当然,就目前我们使用fireEvent也是可以的
- waitFor这个是异步等待,这里之所以使用到,是因为我们需要等到input数据改变之后才能获取值,和setState有关。
下拉列表
下拉列表原本是select/otpion的,但是由于我们这里使用到的是mui框架,因此我们发现它的下拉框其实就是TextField,但是需要加属性select,因此我们对于input的封装修改一下:
import React from 'react';
import type { TextFieldProps } from '@mui/material/TextField';
import type { ReactElement } from 'react';
import TextField from '@mui/material/TextField';
// 类型写法一
type ZLTextProps = TextFieldProps & {
startAdornment?: ReactElement,
endAdornment?: ReactElement,
inputType?: string
}
// 类型写法二
type AA<Type = object> = Type & {
startAdornment?: ReactElement,
endAdornment?: ReactElement
}
type ZLTextProps2 = AA<TextFieldProps>
const ZlInput = (props: ZLTextProps): ReactElement => {
const {
name,
label,
placeholder = 'please Input',
inputType = '',
children,
...rest
} = props;
let show_input = null;
switch(inputType) {
case 'select':
show_input = (
<TextField
id={name}
label={label}
select
placeholder={placeholder}
{...rest}
> {children} </TextField>
)
break;
default:
}
return (
<>
{
inputType ? show_input : (
<TextField
id={name}
label={label}
placeholder={placeholder}
{...rest}
/>
)
}
</>
)
}
export default ZlInput;
TextField可以适用于输入框和选择框,因此我们一起封装。
然后新建一个
page/detail/Com/select.tsx的文件,相当于我们的业务逻辑页面
import ZlInput from "src/component/common/Input";
import MenuItem from '@mui/material/MenuItem';
const Z_select = () => {
const currencies = [
{
value: 'apple',
label: 'apple',
},
{
value: 'banana',
label: 'banana',
},
{
value: 'pear',
label: 'pear',
},
{
value: 'water',
label: 'water',
},
];
return (
<>
<ZlInput
id='like'
label='like'
variant="standard"
data-testid='like'
inputType='select'
defaultValue="apple"
helperText="Please select your like"
>
{currencies.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</ZlInput>
</>
)
}
export default Z_select;
接着我们在page/detail/detail.tsx中添加内容
...
import Z_select from './Com/select';
const Detail = () => {
return (
<>
...
<Z_select />
</>
)
}
export default Detail;
如果您不知道为啥页面结构是这样,建议回顾一下我当时项目搭建的过程:
https://www.toutiao.com/article/7250691387551826432/
由于我们的测试都是写在input.test.tsx中,因此对于一些公共的方法,我们有必要先提取出来。所以新建文件
__tests__/react/utils/index.ts(注意这个ts,后续您可以直直接拿过来用的)。在这个文件里面我们把InputField方法拿过来了,并写了一个点击下拉框的方法:
import { screen } from "@testing-library/dom";
import { fireEvent,within } from "@testing-library/dom";
interface InputProps {
testId?: string,
value?: string
}
export const InputField = (props: InputProps): void => {
const { testId = '' , value } = props;
const el = screen.getByTestId(testId);
const input = el.getElementsByTagName('input')[0];
fireEvent.change(input,{
target: {
value: value
}
})
}
export const getElement = (
testId?: string,
selector?: string
): HTMLElement | null => {
if ( selector ) {
return document.querySelector(selector);
} else if ( testId ) {
return screen.getByTestId(testId)
}
return null;
}
export const getSelectValue = (props: InputProps) => {
const {
testId = '',
value
} = props;
// 先找到testId的节点
const dropDown = within(screen.getByTestId(testId));
// 再找到可以点击的节点,并点击
fireEvent.mouseDown(dropDown.getByRole('button'));
// 此时会出现下啦框
// 找到下拉列表的选项
const listbox = within(screen.getByRole('listbox'));
expect(listbox).not.toBeNull();
// 选择一个点击,这里选择和value一样的节点
const option = listbox.getByText(value as string);
fireEvent.click(option)
}
最后在测试文件 __tests__/react/com/input.test.tsx
...
import { getSelectValue,InputField,getElement } from '../utils/index';
describe('test react com',() => {
...
it('test select',() => {
const Com = (
<Detail />
)
const container = render(Com);
getSelectValue({
testId: 'like',
value: 'pear'
})
expect(container).toMatchSnapshot('detail-input2')
})
})
注意事项:
- 页面中使用React.Fragment测试文件识别不了,我们可以使用<>尖括号替代
- 我们使用getTestById的时候,必须要有一个初始值
- 我们书写下拉框方法的时候是注意了细节的,即页面展示了什么,点击后,会发生什么都会按照我们ui页面真实反馈。大家在一边测试自己项目的时候,也可以打开自己的项目,仔细观察他们的html结构和属性,方便自己获取节点
- within方法是用来包裹一个节点,并将这个节点返回后具有和screen一样的查询方法
复选框checkbox
新建公共组件 component/common/checkbox.tsx ,让复选框都一层封装
import * as React from 'react';
import Checkbox from '@mui/material/Checkbox';
interface Props {
checked?: boolean,
handleChange?: (e: React.ChangeEvent<HTMLInputElement>) => void,
}
export default function Zl_checkbox(props: Props) {
const {
checked = false,
handleChange,
...rest
} = props;
return (
<Checkbox
checked={checked}
onChange={handleChange}
inputProps={{ 'aria-label': 'controlled' }}
{...rest}
/>
);
}
新建功能性组件 page/detail/Com/checkbox.tsx
import { useState } from "react";
import Zl_checkbox from "src/component/common/checkbox";
export default function Check_Box() {
const [checked,setCheck] = useState(false);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCheck(event.target.checked)
}
return (
<>
<Zl_checkbox
checked={checked}
handleChange={handleChange}
data-testid='like_apple'
/>
</>
)
}
我们给测试的
__tests__/react/utils/index.ts新加一个方法,这个方法会把点击复选框之后,会将input的选中状态返回
...
export const changeCheckbox = (props: InputProps): boolean => {
const { testId = '' , value } = props;
const checkbox = screen.getByTestId(testId);
const input = checkbox.getElementsByTagName('input')[0];
fireEvent.click(input);
return input.checked
}
在测试文件 __tests__/react/input.test.tsx多加一个测试
it('test checkbox',() => {
const Com = (
<Detail />
)
// 默认状态是false
const container = render(Com);
const checked = changeCheckbox({
testId: 'like_apple'
})
console.log(checked,'first') // true
const checked2 = changeCheckbox({
testId: 'like_apple'
})
console.log(checked2,'second') // false
})
注意事项:
- 当我们的字组件内容只有一个表单时,最好时通过dom选择器进行获取
- checkbox复选框同样需要是手控组件
- 仔细看,会发现,当我们没有测试点击这块的时候,覆盖率时不会覆盖到handleChange这个事件的,因此需要我们像上面那样,手动某一次操作=
- 注意change事件的类型写法:React.ChangeEvent
radioGroup
同前面类似,先创建一个文件 component/common/radioGroup.tsx
import React , { useState} from 'react';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
interface Obj {
value: string,
label: string
}
interface Props {
id?: string,
dd: Obj[],
name?: string,
head: string,
value?: string,
handleChange: ( event: React.ChangeEvent<HTMLInputElement> ) => void
}
export default function RadioButtonsGroup(props: Props) {
const {
id = 'demo-controlled-radio-buttons-group',
dd,
name,
head,
value = '',
handleChange,
...rest
} = props;
return (
<FormControl>
<FormLabel id={id}>{head}</FormLabel>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
value={value}
name={name}
onChange={handleChange}
>
{
dd.map((item,i) => (
<FormControlLabel key={JSON.stringify(item)} value={item.value} control={<Radio />} label={item.label}/>
))
}
</RadioGroup>
</FormControl>
);
}
然后写一个测试组件 page/detail/Com/radioGroup.tsx
import { useState } from "react";
import RadioButtonsGroup from "src/component/common/radioGroup";
const dd = [
{value: 'boy', label: 'boy'},
{value: 'girl', label: 'girl'}
]
export default function Zl_radioGroup() {
const [val,setVal] = useState('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const isCheck = event.target.checked;
if ( isCheck ) {
setVal(value)
} else {
setVal('')
}
}
return (
<>
<RadioButtonsGroup
value={val}
head='gender'
dd={dd}
handleChange={handleChange}
data-testid='gender123'
/>
</>
)
}
接着放到detail页面中去
...
import Zl_radioGroup from './Com/radioGroup';
const Detail = () => {
return (
<>
...
<div>
<Zl_radioGroup />
</div>
</>
)
}
export default Detail;
测试文件 __tests__/react/utils/index.ts新增内容
...
export const setRadio = (props: InputProps): string => {
const { testId, value } = props;
// 找到局部内容
const randioGroup = screen.getByRole('radiogroup');
// 根据局部内容找到全部的randio
const radioBtns = randioGroup.querySelectorAll('input[type="radio"]');
// 将类数组改成真实数组 注意类型写法
const radioBtns2 = [...radioBtns] as Array<HTMLInputElement> ;
// 根据测试穿件来的值进行查找,并点击
const filRadio = radioBtns2.filter((radioInput: HTMLInputElement) => radioInput.value === value);
fireEvent.click(filRadio[0]);
// 最后将点击的按钮值进行返回
return filRadio[0].value
}
新增测试文件 __test__/react/radioGroup.test.tsx
import { screen } from '@testing-library/dom'
import { render, waitFor } from "@testing-library/react";
import userEvent from '@testing-library/user-event';
// 引入jest-dom的匹配内容
import '@testing-library/jest-dom';
import { setRadio } from '../utils';
import Detail from "src/page/detail/detail";
describe('test react com',() => {
it('test input',() => {
console.log('kkk')
const Com = (
<Detail />
)
const container = render(Com);
const value = setRadio({
value: 'girl'
})
expect(value).toBe('girl');
expect(container).toMatchSnapshot('radio-input')
})
})
注意事项:
- radioGroup这里html结构中有一个role='radioGroup'所以我们找局部结构的时候就用他来
- 注意change事件的写法:handleChange: ( event: React.ChangeEvent ) => void
- querySelectAll返回的是一个类数组,需要转变成真实的数组,这里用到了...
- 数组对象类型:Array
react表单上传
以上是表单的所有内容,那么现场来一个案例试试水~
这里完成的任务主要是将以上所讲内容进行整合,同时发起一次api请求。
首先我们在express/index.js中加入一个post请求:
...
import bodyparser from 'body-parser';
// 解析body
app.use(bodyparser.urlencoded({extended:false}));
app.post('/home',(req,res) => {
const body = req.body;
console.log(body);
res.send({
status: 0,
msg: 'submit success'
})
})
如果您不知道为啥这里突然加了一个借口,建议回顾一下我当时项目搭建的过程:
https://www.toutiao.com/article/7250691387551826432/
接着新建网络请求api/detail.ts文件
import axios from "axios";
const commonUrl = '/api';
interface Obj {
username: string,
age: string,
email: string,
city: string,
like: string,
isLike: boolean
}
export const submitData = async (data: Obj) => {
const path = commonUrl + '/home';
const res = await axios.post<any>(
path,
{...data},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
return res.data
}
然后我们写一个组件 page/detail/Com/submit.tsx
以下代码确实多,可以访问gitee:https://gitee.com/xifeng-canyang/jest-copy-file-and-video/blob/master/src/page/detail/Com/submit.tsx
import React,{useEffect,useState} from "react";
import {
Grid,
styled,
Box,
MenuItem,
Button
} from '@mui/material';
import ZlInput from "src/component/common/Input";
import RadioButtonsGroup from "src/component/common/radioGroup";
import Zl_checkbox from "src/component/common/checkbox";
import { submitData } from "../../../api/detail";
const ItemStyle = styled('div')({
[`& .MuiFormControl-root `]: {
verticalAlign: 'bottom',
marginLeft: '20px'
},
[`& span`]: {
display: 'inline-block',
width: '80px',
textAlign: 'right'
},
width: '100%'
})
const RadioStyle = styled('div')({
display: 'flex',
alignItems: 'center',
[`& .MuiFormControl-root `]: {
verticalAlign: 'bottom',
marginLeft: '20px'
},
[`& .radio-left`]: {
width: '80px',
textAlign: 'right',
},
})
const LikeStyle = styled('div')({
display: 'flex',
alignItems: 'center',
marginTop: '-10px',
[`& .MuiButtonBase-root`]: {
marginLeft: '6px'
},
[`& .radio-left`]: {
width: '80px',
textAlign: 'right',
},
})
const CityStyle = styled('div')({
[`& .MuiFormControl-root `]: {
verticalAlign: 'bottom',
marginLeft: '20px'
},
[`& span`]: {
display: 'inline-block',
width: '80px',
height: '42px',
textAlign: 'right'
},
width: '100%'
})
const SubmitStyle = styled('div')({
[`& .MuiButtonBase-root `]: {
verticalAlign: 'bottom',
marginLeft: '20px'
},
[`& span`]: {
display: 'inline-block',
width: '80px',
height: '42px',
textAlign: 'right'
},
width: '100%'
})
interface Item {
value: string,
label: string
}
interface Obj {
username: string,
age: string,
email: string,
city: string,
like: string,
isLike: boolean
}
const dd = [
{ value: 'apple', label: 'apple'},
{ value: 'apple2', label: 'apple2'},
{ value: 'apple3', label: 'apple3'},
]
const currencies = [
{
value: 'beijing',
label: 'beijing',
},
{
value: 'shanghai',
label: 'shanghai',
},
{
value: 'shenzhen',
label: 'shenzhen',
}
];
export default function Submit() {
const [obj,setObj] = useState<Obj>({
username: '',
age: '',
email: '',
city: '',
like: '',
isLike: false
})
const [like,setLike] = useState('');
const handleLikeOption = (event: React.ChangeEvent<HTMLInputElement>) => {
// console.log(event.target.value,'like');
const target = event.target;
if ( target.checked ) {
setLike(event.target.value);
setObj({
...obj,
like: event.target.value
})
} else {
setLike('')
}
}
const City = () => {
return (
<ZlInput id="city" data-testid='city' inputType='select' label='city' handleInputChange={changeCity} placeholder="please input" defaultValue={currencies[0].value} variant="standard" helperText="Please select your city" sx={{ width: '400px'}}>
{currencies.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</ZlInput>
)
}
const changeCity = (event: React.ChangeEvent<HTMLInputElement>) => {
setObj({
...obj,
city: event.target.value
})
}
const handleChangeLike = (event: React.ChangeEvent<HTMLInputElement>) => {
setObj({
...obj,
isLike: event.target.checked
})
}
const handleSubmit = async () => {
console.log('--submit data--',obj)
const res = await submitData(obj)
console.log(res,'submit')
}
const changeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
if ( e.target.labels?.length ) {
// 这里有一个BUG,明明给了age输入框一个label,但是labels获取的NodeList为空数组,其他的却正常
const labelValue = e.target?.labels[0]?.textContent;
switch(labelValue) {
case 'username':
setObj({
...obj,
username: val
})
break;
case 'age':
setObj({
...obj,
age: val
})
break;
case 'email':
setObj({
...obj,
email: val
})
}
} else {
// console.log(e.target.labels,'kkk')
setObj({
...obj,
age: val
})
}
}
return (
<Box sx={{ display: 'flex', flexWrap: 'wrap', width: '800px' }}>
<Grid container spacing={2} sx={{ padding: '20px'}}>
<Grid item xs={12}>
<ItemStyle>
<span>username: </span><ZlInput label='username' data-testid='username' handleInputChange={changeChange} defaultValue={obj.username} id="usename" placeholder="please input" variant="standard" sx={{ width: '400px'}} />
</ItemStyle>
</Grid>
<Grid item xs={12}>
<ItemStyle>
<span>age: </span><ZlInput id="age" label='age' data-testid='age' handleInputChange={changeChange} defaultValue={obj.age} placeholder="please input" variant="standard" sx={{ width: '400px'}} />
</ItemStyle>
</Grid>
<Grid item xs={12}>
<ItemStyle>
<span>email: </span><ZlInput id="email" data-testid='email' handleInputChange={changeChange} label='email' defaultValue={obj.email} placeholder="please input" variant="standard" sx={{ width: '400px'}} />
</ItemStyle>
</Grid>
<Grid item xs={12}>
<CityStyle>
<span>city: </span><City />
</CityStyle>
</Grid>
<Grid item xs={12}>
<RadioStyle>
<div className="radio-left">like: </div><RadioButtonsGroup data-testid='likeOptions' id="like-btn" dd={dd} handleChange={handleLikeOption} value={like} />
</RadioStyle>
</Grid>
<Grid item xs={12}>
<LikeStyle>
<div className="radio-left">like apple: </div><Zl_checkbox
checked={obj.isLike}
handleChange={handleChangeLike}
data-testid='like_apple'
/>
</LikeStyle>
</Grid>
<Grid item xs={12}>
<SubmitStyle>
<span></span>
<Button variant="contained" data-testid='submit' color="success" onClick={handleSubmit}>Submit</Button>
</SubmitStyle>
</Grid>
</Grid>
</Box>
)
}
看起来写了很长,但是仔细阅读,无非就是一些表单子内容,我们这里用到的是mui,可以赋值跑通,结果大概长这个样子:

最后我们就可以写测试文件__tests__/react/com/submit.test.tsx
import { fireEvent, screen } from '@testing-library/dom'
import { render, waitFor } from "@testing-library/react";
import * as utils from '../../../api/detail';
jest.mock('../../../api/detail');
const submitDD = jest.spyOn(utils,'submitData');
submitDD.mockImplementation(
(): Promise<any> => {
return new Promise((resolve) => {
resolve({
status: 0,
msg: 'submit success2'
})
})
}
)
// 引入jest-dom的匹配内容
import '@testing-library/jest-dom';
import {
setRadio,
getSelectValue,
InputField,
changeCheckbox
} from '../utils';
import Submit from 'src/page/detail/Com/submit';
describe('test react submit', () => {
it('test input',async () => {
console.log('kkk')
const Com = (
<Submit />
)
const container = render(Com);
// 三个输入框
InputField({
testId: 'username',
value: 'jack'
})
InputField({
testId: 'age',
value: '12'
})
InputField({
testId: 'email',
value: '111@qq.com'
})
// 选择城市
getSelectValue({
testId: 'city',
value: 'shanghai'
})
// 选择喜欢项
const likeValue = setRadio({
value: 'apple2'
})
// 确定是否要apple
const checked2 = changeCheckbox({
testId: 'like_apple'
})
// 提交
const submitBtn = screen.getByTestId('submit');
fireEvent.click(submitBtn);
await waitFor(() => {
expect(submitDD).toHaveBeenCalled();
})
expect(container).toMatchSnapshot('submit-input')
})
})
当我们成功跑通的时候,我们就会看到如下的打印

注意事项:
- express注意解析body,引入body-parser
- axios发起post请求时,我们携带的data如果想被express解析,那么需要加一个头:headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
- 注意我这里是新建了一个detail.ts来写detail页面的网络请求,那么为什么我不直接写在home.ts里面呢?原因是我测试的时候,发现如果我写在homt.ts中时,可能由于之前我们使用了自定义mock,home.ts已经被__mocks__/home.ts所管理,导致页面无法访问post请求的这个方法
组件详谈
表单组件基本元素为这些了,那么对于列表list以及表格这块怎么说呢,其实这两个我们只需要给个初始值,他能够正常渲染,覆盖率基本上就很高了,而表格的一些高级操作,比如过滤,搜索等,如果你写了方法,那么确实要测试一下,这个实现思路跟utils/index.ts里面的方法类似,模拟的时候操作了一遍,就能覆盖到。
这么艰难的看到这里,帮忙点点赞支持~