0


react单元测试

单元测试

  • 针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。最小单位: main / userPart正确性检验: 验证 预期结果 与 输出结果 是否一致

测试作用

  • 保证代码质量 提高效率
  • 更早的发现bug, 降低bug出现与复现
  • 增强开发者信心

测试思想

  • TDD: Test-Driven Development(测试驱动开发) 编写某个功能的代码之前先编写测试代码,仅编写使测试通过的功能代码,通过测试来推动整个开发的进行
  • BDD: Behavior-Driven Development(行为驱动开发)使用自然语言来描述系统功能和业务逻辑,根据描述步骤进行功能开发,然后编写的测试代码

测试类型

  • 单元测试(Unit Test) 作用: 保证最小单位的代码与预期结果一致性 应用: 公共函数,单个组件
  • 集成测试(Integration Test) 作用: 测试经过单元测试后的各个模块组合在一起是否能正常工作 应用: 耦合度较高的函数/组件、二次封装的函数/组件、多个函数/组件组合而成的代码
  • 界面测试(UI Test) 作用: 脱离真实后端环境,程序中数据来源通过Mock模拟 应用: 开发过程中的自测
  • 端到端测试(E2E test) 作用: 整个应用程序在真实环境中运行,数据来源后端 应用: 测试工程师手工测试与自动测试

React 测试库搭配

  • Airbnb Enzyme+chai+sinon+jest enzyme: 模拟react组件运行及输出, 操作、遍历 chai: BDD / TDD 断言库,适用于节点和浏览器,可以与任何 js 测试框架搭配 sinon: 具有可以模拟 spies, stub, mock 功能的库
  • Testing-library testing-library/react + testing-library/jest-dom + testing-library/user-event + jest testing-library/react: 将 React 组件渲染为DOM testing-library/jest-dom: 增加额外的 DOM Matchers testing-library/user-event: 浏览器交互模拟(事件模拟库)

测试文件定义

在这里插入图片描述

单元测试思路

  • 准备数据
  • 渲染组件
  • 断言结果

jest学习 – 匹配器使用

test('精准匹配',()=>{expect(2+2).toBe(4)})test('对象匹配',()=>{const data ={one:1}
    data['two']=2expect(data).toEqual({one:1,two:2})})test('相反匹配',()=>{const a =10const b =20expect(a + b).not.toBe(50)})test('布尔匹配',()=>{constB=nullexpect(B).toBeFalsy()expect(B).toBeNull()expect(B).not.toBeUndefined()expect(B).not.toBeTruthy()})test('等价匹配',()=>{constA=2,B=2expect(A+B).toBeGreaterThan(3)expect(A+B).toBeLessThan(5)constF=0.1+0.2expect(F).toBeCloseTo(0.3)})test('字符串匹配',()=>{const str ='abcd'expect(str).toMatch(/ab/)})test('数组匹配',()=>{const List =['hello','world','one','two','three','four']expect(List).toContain('one')})functionErr(){thrownewError('抛出错误')}test('错误匹配',()=>{expect(()=>Err()).toThrow(/错误/)})

testing-library 学习 – 节点查询

  • 官方文档 https://testing-library.com/docs/queries/about/#types-of-queries
  • 单节点查询getByText : 查询匹配节点,没有或者找到多个会报错queryByText: 查询匹配节点,没有匹配到返回null(主要用于断言不存在的元素),找到多个抛出错误getAllByText: 返回一个promise, 找到匹配的元素时解析成一个元素,未找到或超时(1秒),找到多个都会报错
  • 多节点查询queryAllByText: 返回查询的所有匹配节点的数组,如果没有元素匹配则抛出错误findByText: 返回查询的所有匹配节点的数组,如果没有元素匹配,则返回空数组findAllByText: 返回一个promise,当找到与给定查询匹配的任何元素时,它会解析为一个元素数组。如果在默认超时 1000 毫秒后没有找到任何元素,则该承诺将被拒绝在这里插入图片描述

差异对比

import{ fireEvent }from'@testing-library/react'import userEvent from'@testing-library/user-event'

上面

fireEvent

userEvent

有很多相似的地方, 实际上,

userEvent

是对

fireEvent

补充,

userEvent

从用户角度模拟交互行为

测试场景学习

debug 方法

import{screen, render}from'@testing-library/react'
screen.debug()const{ debug }=render(<Demo click={fn}/>)debug()

点击测试

点击模拟 - 被测文件

import React,{ useState }from'react'export type ButtonProps ={
    onClick?:()=>void}constButton=(props: ButtonProps)=>{const[btnState, setBtnState]= useState<boolean>(false)constinSideClick=()=>setBtnState((state)=>!state)return(<div><button onClick={props.onClick('112')}>Click</button><button onClick={inSideClick} data-testid="toggle">{btnState ?'点击了':'未点击'}</button></div>)}exportdefault Button

点击模拟 - 测试用例

import React from'react'import{ render, screen, fireEvent }from'@testing-library/react'import Button,{ ButtonProps }from'./index'constBTNProps: ButtonProps ={onClick: jest.fn(),}describe('onClick 测试',()=>{test('传入点击',()=>{// 渲染 React DOMrender(<Button {...BTNProps}></Button>)// 在 screen 找到需要断言的元素const element = screen.getByText('Click')as HTMLButtonElement
        // 断言 结果与预期一致性expect(element.tagName).toEqual('BUTTON')// 模拟点击
        fireEvent.click(element)// 断言 是否已经被点击expect(BTNProps.onClick).toHaveBeenCalled()// 被点击次数expect(BTNProps.onClick).toBeCalledTimes(1)// 点击传参测试expect(BTNProps.onClick).toBeCalledWith('112')// 参数})// 内部点击事件是否触发,可以通过DOM变化,间接测试test('内部点击',()=>{// 渲染 React domrender(<Button></Button>)// 通过 test id 获取渲染树元素const element = screen.getByTestId('toggle')as HTMLButtonElement
        // 断言 '未点击' 文本内容是否存在expect(element).toHaveTextContent('未点击')// 模拟点击
        fireEvent.click(element)// 断言 是否点击expect(element).toHaveTextContent('点击了')})})

快照测试

被测文件: 这里使用上面模拟点击的 被测文件

// 测试用例import React from'react'import{ render, screen }from'@testing-library/react'import Button from'../onClick/index'test('快照测试',()=>{render(<Button></Button>)const element = screen.getByTestId('toggle')as HTMLButtonElement
    expect(element).toMatchSnapshot()})

测试结果: 生成一个__snapshots__文件夹
在这里插入图片描述

input 测试

import React from'react'import{ fireEvent, render, screen }from'@testing-library/react'import userEvent from'@testing-library/user-event'// 被测试组件functionDemo(props: any){return(<><button onClick={()=> props?.click('112')}>click</button><input type="text" data-testid="input"/><input type="text" data-testid="blur"/></>)}// input 测试用例describe('input 测试',()=>{test('input',async()=>{// 渲染组件render(<Demo />)// 获取节点const input = screen.getByTestId('input')as HTMLInputElement
        // 模拟输入
        fireEvent.change(input,{target:{value:'1223'}})// 判断值expect(input.value).toBe('1223')})test('blur',async()=>{// 渲染组件render(<Demo />)// 获取节点const input = screen.getByTestId('blur')as HTMLInputElement
        // 模拟输入激活焦点
        input.blur()})test('userEvent input',()=>{const fn = jest.fn()const{ container, debug }=render(<Demo click={fn}/>)// 断点使用debug()// 查找 DOM const btn = container.querySelector('button')as HTMLButtonElement
        // 模拟事件点击
        userEvent.click(btn)// 调用 次数expect(fn).toBeCalledTimes(1)})})

select option 测试

import React,{ useCallback, useState }from'react'import{ fireEvent, render, screen, waitFor }from'@testing-library/react'import userEvent from'@testing-library/user-event'import{ act }from'react-dom/test-utils'test('selectOptions',async()=>{render(<select multiple><option value="1">A</option><option value="2">B</option><option value="3">C</option></select>,)await userEvent.selectOptions(screen.getByRole('listbox'),['1','C'])expect(screen.getByRole('option',{name:'A'}).selected).toBe(true)expect(screen.getByRole('option',{name:'B'}).selected).toBe(false)expect(screen.getByRole('option',{name:'C'}).selected).toBe(true)})
import React,{ useCallback, useState }from'react'import{ fireEvent, render, screen, waitFor }from'@testing-library/react'import userEvent from'@testing-library/user-event'import{ act }from'react-dom/test-utils'test('deselectOptions',async()=>{render(<select multiple><option value="1">A</option><option value="2" selected>B</option><option value="3">C</option></select>,)await userEvent.deselectOptions(screen.getByRole('listbox'),'2')expect(screen.getByText('B').selected).toBe(false)})

定时器模拟

import React,{ useCallback, useState }from'react'import{ fireEvent, render, screen, waitFor }from'@testing-library/react'import userEvent from'@testing-library/user-event'import{ act }from'react-dom/test-utils'// 被测试组件functionDemo(){const[flag, setFlag]=useState(false)const clickHandle =useCallback(()=>{setFlag(true)setTimeout(()=>{setFlag(false)},2000)},[setFlag])return(<><button className={`${flag ?'disabled':''}`} onClick={clickHandle}>
                click
            </button></>)}// 模拟定时器测试describe('mock time',()=>{test('setTimeout',async()=>{// 模拟定时器
        jest.useFakeTimers()// 渲染组件render(<Demo />)// 查找元素const btn = screen.getByRole('button')// 断言expect(btn).not.toHaveClass('disabled')// 模拟点击
        userEvent.click(btn)// 断言 dom 中是否包含 classexpect(btn).toHaveClass('disabled')// act 是 test-utils 的一个异步方法act(()=>{// 模拟 准确时间// jest.advanceTimersByTime(2000)// 模拟所有的定时器
            jest.runAllTimers()})expect(btn).not.toHaveClass('disabled')// debug 方法})})

re render 测试

import{ render }from'@testing-library/react'import React from'react'functionDemo(props){return<div>{props.num}</div>}test('re render',()=>{const{ container, debug, rerender }=render(<Demo num={2}/>)expect(container.querySelector('div').textContent).toBe('2')// 通过 rerender 字段实现重新渲染rerender(<Demo num={5}/>)expect(container.querySelector('div').textContent).toBe('5')})

自定义 hooks 测试

通过 @testing-library/react-hooks 这个库实现自定义hooks测试

import{ renderHook, act }from'@testing-library/react-hooks'import React,{ useEffect, useState }from'react'functionuseSum(init){const[count, setCount]=useState(init)const[resNum, setResNum]=useState()useEffect(()=>{setResNum(count +10)},[count])return{ resNum, setCount }}test('hooks',()=>{const{ result }=renderHook(()=>useSum(0))expect(result.current.resNum).toBe(10)act(()=>{
        result.current.setCount(100)})expect(result.current.resNum).toBe(110)})

复用逻辑测试

被测试组件

import React from'react'enum Types {
    red ='red',
    green ='green',
    blue ='rgb(34, 35, 35)',}functionDemo(props){return(<><button style={{background: props.types }}>btn</button></>)}

正常测试

import{ render, screen, waitFor }from'@testing-library/react'import{ act }from'react-dom/test-utils'enum Types {
    red ='red',
    green ='green',
    blue ='rgb(34, 35, 35)',}test('btn background',async()=>{const{ rerender }=render(<Demo types={'red'}/>)expect(screen.getByRole('button').style.background).toBe(Types.red)act(()=>{rerender(<Demo types={'green'}/>)})expect(screen.getByRole('button').style.background).toBe(Types.green)rerender(<Demo types={'#222323'}/>)expect(screen.getByRole('button').style.background).toBe(Types.blue)})

使用

test.each

简化测试

enum Types {
    red ='red',
    green ='green',
    blue ='rgb(34, 35, 35)',}
test.each([['red', Types.red],['green', Types.green],['#222323', Types.blue],])('test each',(type, expected)=>{render(<Demo types={type}/>)expect(screen.getByRole('button').style.background).toBe(expected)})

测试 redux

测试 redux - 被测文件

import React from'react'import{ useSelector }from'react-redux'import{ useHistory }from'react-router-dom'export type StoreType ={userInfo:{age: number
        name: string
        id: string
    }}constUserInfoPart=()=>{const userInfo =useSelector((store: StoreType)=> store.userInfo)const jump =useHistory()constjumpHandle=()=> jump.push('a/b/c')return(<div><h3 onClick={jumpHandle}>ID:{userInfo.id}</h3><p>姓名:{userInfo.name}</p><p>年龄:{userInfo.age}</p></div>)}exportdefault UserInfoPart

测试 redux - 测试文件
通过

redux-mock-store

库 实现 redux 模拟测试

import React from'react'import{ render, screen }from'@testing-library/react'import UserInfoPart,{ StoreType }from'./index'import configureStore from'redux-mock-store'import{ Provider }from'react-redux'// 定义初始化数据constinitState: StoreType ={userInfo:{age:18,name:'xiaoming',id:'xm-110-2',},}// store 数据模拟const mockStore =configureStore([])const store =mockStore(initState)describe('模拟redux',()=>{test('验证姓名,年龄',()=>{render(<Provider store={store}><UserInfoPart /></Provider>)const element = screen.getByText('姓名: xiaoming')const el = screen.getByText(`ID: xm-110-2`)expect(el.tagName).toBe('H3')expect(element.tagName).toEqual('P')})})

请求测试

被测组件

import React,{ useCallback, useEffect, useState }from'react'functionDemo(){const[data, setData]=useState(['11'])const[err, setErr]=useState('')const clickHandle =useCallback(async()=>{fetch('/user/submit',{method:'POST',body:JSON.stringify({useranme:'123',}),}).then((res)=>{if(res.status ===400){
                console.log(res.status)setErr('提交错误')}})},[setErr])constfetchData=()=>{fetch('/list',{method:'POST',}).then((res: any)=>{return res.json()}).then((res: any)=>{setData(res)})}useEffect(()=>{fetchData()},[fetchData])return(<><span data-testid="err">{err}</span><button onClick={clickHandle}>click</button><div><ul data-testid="list">{data.length &&
                        data.map((i: string,index: number)=>{return<li key={index}>{i}</li>})}</ul></div></>)}exportdefault Demo

测试用例
模拟请求 需要通过

msw

库实现

import React from'react'import{ fireEvent, render, screen, waitFor }from'@testing-library/react'import userEvent from'@testing-library/user-event'import{ setupServer }from'msw/node'import{ rest }from'msw'import Demo from'./list'import{ act }from'react-dom/test-utils'import{ runServer }from'./mocks'runServer(beforeAll, afterAll, afterEach)// const server = setupServer()// const server = setupServer(//     rest.post('/list', (req, res, ctx) => {//         return res(ctx.json(['1', '2', '3']))//     })// )// beforeAll(() => server.listen())// afterAll(() => server.close())// afterEach(() => server.resetHandlers())test('data',async()=>{render(<Demo />)awaitwaitFor(()=>{const list = screen.getByTestId('list')expect(list.children).toHaveLength(3)})})// test('submit', async () => {//     render(<Demo />)//     // await waitFor(() => {//     //     const list = screen.getByTestId('list')//     //     expect(list.children).toHaveLength(3)//     // })//     await waitFor(() => {//         const btn = screen.getByRole('button')//         userEvent.click(btn)//     })//     await waitFor(() => {//         const err = screen.getByTestId('err')//         expect(err.textContent).toBe('提交错误')//     })// })

mock 使用

父组件 : Index

import React from"react";// 引入子组件import Child from"child";constIndex=()=>{functioncallBack(message: string =""){
        console.log(`来自子组件的消息是:${message}`);}return(<div className="jest-demo"><Child callBack={callBack}/></div>);};exportdefault Index;

子组件 child

import React,{ useEffect }from"react";

type iPropsType ={callBack: Function;};constChild=(props: iPropsType)=>{useEffect(()=>{
        props.callBack("我是正经的子组件");},[]);return<div>子组件</div>;};exportdefault Child;

测试用例

import React from"react";import{ render }from"@testing-library/react";import JestDemo from"../index";// 注意这里 child 组件是需要被模拟的, 使用 mock_component 组件代替 child 组件
jest.mock("./child",()=>require("./mock_component").default);describe("组件mock单测",()=>{test("mock组件",async()=>{const{ container }=render(<JestDemo />);expect()// 断言逻辑});});

mock的组件 : mock_component

import React,{ useEffect }from"react";

type iPropsType ={callBack: Function;};constIndex=(props: iPropsType)=>{useEffect(()=>{
        props.callBack("我是MOCK的子组件");},[]);return<div>页面</div>;};exportdefault Index;
标签: jest 单元测试 react

本文转载自: https://blog.csdn.net/bianliuzhu/article/details/123878846
版权归原作者 Gleason. 所有, 如有侵权,请联系我们删除。

“react单元测试”的评论:

还没有评论