jest测试react组件

以下为本节内容,内容比较多,且都是日常工作中会用到的测试用例与常见的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输入框照结果

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里面的方法类似,模拟的时候操作了一遍,就能覆盖到。

这么艰难的看到这里,帮忙点点赞支持~

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容