以下為本節(jié)內(nèi)容,內(nèi)容比較多,且都是日常工作中會用到的測試用例與常見的react組件用例,相信您會有所收獲的
- 測試React
- 從一個簡單的測試開始
- 運行條件
- 跑通成功
- react組件測試
- input輸入框
- input輸入框不使用UI框架下
- 下拉列表
- 復(fù)選框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
模塊依賴解釋,和對應(yīng)的文檔位置:https://testing-library.com/docs/
這三個依賴包我們會用到的功能有這么幾個,這幾個基本上可以解決很多測試功能的問題。
// cleanup清除,一般用于全局的afterAll中
// fireEvent操作事件,如點擊等
// render渲染組件,返回一個container,可用于生成快照
// waitFor如果渲染過程中涉及異步操作,那么數(shù)據(jù)的返回就必須要使用waitFor
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
// screen == container.screen,用于獲取查詢方法
import { screen } from '@testing-library/dom';
// userEvent同fireEvent但高于fireEvent,我們推薦優(yōu)先使用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')
})
})
運行條件
- 確保環(huán)境是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快照

快照圖還是比較有用的,在大型項目中測試通過與否會比較兩次測試的結(jié)果是否會發(fā)生變化,如果發(fā)生了變化會認為測試失?。贿@就意味著我們不僅要上傳最新的測試用例,同時對于業(yè)務(wù)中出現(xiàn)的時間等任何可能改變頁面內(nèi)容的東西,全部mock
react組件測試
主要是測試一些表單控件,暫時都在detail這個頁面測試吧。這里我們使用的輸入框為mui,因此需要安裝一下
npm install @mui/material @emotion/react @emotion/styled // 組件
npm install @mui/icons-material // icon
官網(wǎng):https://mui.com/material-ui/getting-started/installation/
好,現(xiàn)在直接開擼~
input輸入框
目的:渲染一個頁面,這個頁面包含了input輸入框,我們需要給輸入框添加內(nèi)容。
封裝一個公共的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所對應(yīng)的dom節(jié)點,然后根據(jù)這個節(jié)點我們繼續(xù)往下面找子節(jié)點內(nèi)容
const input = el.getElementsByTagName('input')[0];
fireEvent.change(input,{
target: {
value: value
}
})
}
// 兩種方法獲取dom節(jié)點
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')
})
})
測試內(nèi)容講解
render
用于渲染組件,以便生成快照
fireEvent
用于處理事件,包括點擊、聚焦、改變等
screen
獲取testing-libriary的dom方法,包括三大類,查詢query,獲取get,和查找find
官網(wǎng)地址: https://testing-library.com/docs/queries/about
InputField
用于改變input框的輸入值,即改變value值
getElement
用于獲取dom節(jié)點
快照

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,官網(wǎng):https://testing-library.com/docs/user-event/intro
- 這里的type是輸入值的意思
- 這里的toHaveValue匹配器并不是jest所有的,是我們引入了jest/dom:import '@testing-library/jest-dom' 才有的,大家可以去看源碼,相信我會收獲不少的。當然,就目前我們使用fireEvent也是可以的
- waitFor這個是異步等待,這里之所以使用到,是因為我們需要等到input數(shù)據(jù)改變之后才能獲取值,和setState有關(guān)。
下拉列表
下拉列表原本是select/otpion的,但是由于我們這里使用到的是mui框架,因此我們發(fā)現(xiàn)它的下拉框其實就是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的文件,相當于我們的業(yè)務(wù)邏輯頁面
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中添加內(nèi)容
...
import Z_select from './Com/select';
const Detail = () => {
return (
<>
...
<Z_select />
</>
)
}
export default Detail;
如果您不知道為啥頁面結(jié)構(gòu)是這樣,建議回顧一下我當時項目搭建的過程:
https://www.toutiao.com/article/7250691387551826432/
由于我們的測試都是寫在input.test.tsx中,因此對于一些公共的方法,我們有必要先提取出來。所以新建文件
__tests__/react/utils/index.ts(注意這個ts,后續(xù)您可以直直接拿過來用的)。在這個文件里面我們把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的節(jié)點
const dropDown = within(screen.getByTestId(testId));
// 再找到可以點擊的節(jié)點,并點擊
fireEvent.mouseDown(dropDown.getByRole('button'));
// 此時會出現(xiàn)下啦框
// 找到下拉列表的選項
const listbox = within(screen.getByRole('listbox'));
expect(listbox).not.toBeNull();
// 選擇一個點擊,這里選擇和value一樣的節(jié)點
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的時候,必須要有一個初始值
- 我們書寫下拉框方法的時候是注意了細節(jié)的,即頁面展示了什么,點擊后,會發(fā)生什么都會按照我們ui頁面真實反饋。大家在一邊測試自己項目的時候,也可以打開自己的項目,仔細觀察他們的html結(jié)構(gòu)和屬性,方便自己獲取節(jié)點
- within方法是用來包裹一個節(jié)點,并將這個節(jié)點返回后具有和screen一樣的查詢方法
復(fù)選框checkbox
新建公共組件 component/common/checkbox.tsx ,讓復(fù)選框都一層封裝
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新加一個方法,這個方法會把點擊復(fù)選框之后,會將input的選中狀態(tài)返回
...
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 />
)
// 默認狀態(tài)是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
})
注意事項:
- 當我們的字組件內(nèi)容只有一個表單時,最好時通過dom選擇器進行獲取
- checkbox復(fù)選框同樣需要是手控組件
- 仔細看,會發(fā)現(xiàn),當我們沒有測試點擊這塊的時候,覆蓋率時不會覆蓋到handleChange這個事件的,因此需要我們像上面那樣,手動某一次操作=
- 注意change事件的類型寫法:React.ChangeEvent
radioGroup
同前面類似,先創(chuàng)建一個文件 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新增內(nèi)容
...
export const setRadio = (props: InputProps): string => {
const { testId, value } = props;
// 找到局部內(nèi)容
const randioGroup = screen.getByRole('radiogroup');
// 根據(jù)局部內(nèi)容找到全部的randio
const radioBtns = randioGroup.querySelectorAll('input[type="radio"]');
// 將類數(shù)組改成真實數(shù)組 注意類型寫法
const radioBtns2 = [...radioBtns] as Array<HTMLInputElement> ;
// 根據(jù)測試穿件來的值進行查找,并點擊
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的匹配內(nèi)容
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結(jié)構(gòu)中有一個role='radioGroup'所以我們找局部結(jié)構(gòu)的時候就用他來
- 注意change事件的寫法:handleChange: ( event: React.ChangeEvent ) => void
- querySelectAll返回的是一個類數(shù)組,需要轉(zhuǎn)變成真實的數(shù)組,這里用到了...
- 數(shù)組對象類型:Array
react表單上傳
以上是表單的所有內(nèi)容,那么現(xiàn)場來一個案例試試水~
這里完成的任務(wù)主要是將以上所講內(nèi)容進行整合,同時發(fā)起一次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/
接著新建網(wǎng)絡(luò)請求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為空數(shù)組,其他的卻正常
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>
)
}
看起來寫了很長,但是仔細閱讀,無非就是一些表單子內(nèi)容,我們這里用到的是mui,可以賦值跑通,結(jié)果大概長這個樣子:

最后我們就可以寫測試文件__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的匹配內(nèi)容
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發(fā)起post請求時,我們攜帶的data如果想被express解析,那么需要加一個頭:headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
- 注意我這里是新建了一個detail.ts來寫detail頁面的網(wǎng)絡(luò)請求,那么為什么我不直接寫在home.ts里面呢?原因是我測試的時候,發(fā)現(xiàn)如果我寫在homt.ts中時,可能由于之前我們使用了自定義mock,home.ts已經(jīng)被__mocks__/home.ts所管理,導(dǎo)致頁面無法訪問post請求的這個方法
組件詳談
表單組件基本元素為這些了,那么對于列表list以及表格這塊怎么說呢,其實這兩個我們只需要給個初始值,他能夠正常渲染,覆蓋率基本上就很高了,而表格的一些高級操作,比如過濾,搜索等,如果你寫了方法,那么確實要測試一下,這個實現(xiàn)思路跟utils/index.ts里面的方法類似,模擬的時候操作了一遍,就能覆蓋到。
這么艱難的看到這里,幫忙點點贊支持~