0


jest单元测试——项目实战

jest单元测试——项目实战

温故而知新:单元测试工具——JEST
包括:什么是单元测试、jest的基本配置、快照测试、mock函数、常用断言、前端单测策略等等。。

一、纯函数测试

关于纯函数的测试,之前的文章讲的蛮多了,这次重点就不在这里了,感兴趣的同学请移步 温故而知新~🎉

// demo.ts/**
 * 比较两个数组内容是否相同
 * @param {Array} arr1 - 第一个数组
 * @param {Array} arr2 - 第二个数组
 * @returns {Boolean} - 如果两个数组内容相同,返回 true,否则返回 false
 */exportconstcompareArrays=(arr1: ReactText[], arr2: ReactText[])=>{if(arr1.length !== arr2.length){returnfalse}else{const result = arr1.every((item)=> arr2.includes(item))return result
  }}//demo.test.tsdescribe('compareArrays',()=>{test('should return true if two arrays are identical',()=>{const arr1 =[1,2,3]const arr2 =[1,2,3]expect(compareArrays(arr1, arr2)).toBe(true)})test('should return false if two arrays have different lengths',()=>{const arr1 =[1,2,3]const arr2 =[1,2,3,4]expect(compareArrays(arr1, arr2)).toBe(false)})// 好多好多用例,我就不每个都展示出来了})

二、组件测试

虽然 Jest 可以对 React 组件进行测试,但不建议在组件上编写太多的测试,任何你想测试的内容,例如业务逻辑,还是建议从组件中独立出来放在单独的函数中进行函数测试,但测试一些 React 交互是很有必要的,例如要确保用户在单击某个按钮时是否正确地调用特定函数。

1. 准备工作——配置 🔧

下载

@testing-library/jest-dom

包:

npm install @testing-library/jest-dom --save-dev

同时,要在

tsconfig.json

里引入这个库的类型声明:

{"compilerOptions":{"types":["node","jest","@testing-library/jest-dom"]}}

为了防止引入 css 文件报错:

npm install --dev identity-obj-proxy

在项目根目录下创建jest.config.js文件:

module.exports ={
  collectCoverage:true,// 是否显示覆盖率报告
  testEnvironment:'jsdom',// 添加 jsdom 测试环境
  moduleNameMapper:{'^@/(.*)$':'<rootDir>/src/$1','\\.(css|scss)$':'identity-obj-proxy',},}

2. 开始测试——写用例 📝

先用小小的 button 试试水~

describe('Button component',()=>{// 测试按钮文案test('should have correct text content',()=>{const{ getByText }=render(<button>Click me</button>)expect(getByText('Click me')).toBeInTheDocument()})// 使用自定义的匹配器断言 DOM 状态test('should be disabled when prop is set',()=>{const{ getByTestId }=render(<button disabled data-testid="button">
        Click me
      </button>)expect(getByTestId('button')).toBeDisabled()})// 模拟点击事件test('should call onClick when clicked',()=>{const handleClick = jest.fn()const{ getByText }=render(<button onClick={handleClick}>Click me</button>)

    fireEvent.click(getByText('Click me'))expect(handleClick).toHaveBeenCalled()})})

接下来是业务组件:

// demo.tsximport React from'react'import'./index.scss'interfaceProps{
  title:string
  showStar?:boolean}const Prefix ='card-title'exportconstCardTitle=(props: Props)=>{const{ title, showStar =true}= props

  return(<div className={`${Prefix}-title`}>{showStar &&<span className={`${Prefix}-title-star`}>*</span>}<div>{title}</div></div>)}// demo.test.tsximport React from'react'import{ render, screen }from'@testing-library/react'import'@testing-library/jest-dom/extend-expect'describe('CardTitle',()=>{it('should have correct text content',()=>{const{ getByText }=render(<CardTitle title="测试标题"/>)expect(getByText('测试标题')).toBeInTheDocument()})it('should render a span if showStar is true',()=>{const{ getByText }=render(<CardTitle title="test" showStar={true}/>)expect(getByText('*')).toBeInTheDocument()})it('should not render a span if showStar is false',()=>{render(<CardTitle title="测试标题" showStar={false}/>)const span = screen.queryByText('*')expect(span).not.toBeInTheDocument()})})

三、接口测试

在测试的时候我们常常希望: 把接口mock掉,不真正地发送请求到后端,自定义接口返回的值。

// api.ts(接口)exportconstgetUserRole=async()=>{const result =await axios.post('XXX',{ data:'abc'})return result.data
}// index.ts(调用函数)exportconstgetUserType=async()=>{const result =awaitgetUserRole()return result
}

1. Mock axios
这种方法可以在不同的测试用例中,根据我们的需要,来控制接口 data 的返回:

it('mock axios',async()=>{
  jest.spyOn(axios,'post').mockResolvedValueOnce({
    data:{ userType:'user'},})const{ userType }=awaitgetUserType()expect(userType).toBe('user')})

2. Mock API
另一种方法是 Mock测试文件中的接口函数:

import*as userUtils from'./api'it('mock api',async()=>{
  jest.spyOn(userUtils,'getUserRole').mockResolvedValueOnce({ userType:'user'})const{ userType }=awaitgetUserType()expect(userType).toBe('user')})

3. Mock Http请求
我们可以不 Mock 任何函数实现,只对 Http 请求进行 Mock!先安装 msw:

🔧 msw 可以拦截指定的 Http 请求,有点类似 Mock.js,是做测试时一个非常强大好用的 Http Mock 工具。

npm install msw@latest--save-dev

需要说明一点,2.0.0以上的版本都是需要node>18的,由于不方便升级,我这里使用的是1.3.3版本(2024-03-15更新的,还是蛮新的哈)

如果你想在某个测试文件中想单独指定某个接口的 Mock 返回, 可以使用 server.use(mockHandler) 。

这里声明了一个 setup 函数,用于在每个用例前初始化 Http 请求的 Mock 返回。通过传不同值给 setup 就可以灵活模拟测试场景了。

import{ rest }from'msw'import{ setupServer }from'msw/node'describe('getUserType',()=>{// 需要mock的接口地址const url ='http://xxxx'const server =setupServer()constsetup=(data:{ userType:string})=>{
    server.use(
      rest.post(url,async(req, res, ctx)=>{returnres(ctx.status(200), ctx.json(data))}))}beforeAll(()=>{
    server.listen()})afterEach(()=>{
    server.resetHandlers()})afterAll(()=>{
    server.close()})it('mock http',async()=>{setup({ userType:'user'})const{ userType }=awaitgetUserType()expect(userType).toBe('user')})})

四、React Hook测试

如果我们需求中需要实现一个 Hook,那么我们要对 Hook 进行测试该怎么办呢?
🌰 举个例子:这里有一个useCounter,提供了增加、减少、设置和重置功能:

import{ useState }from'react'exportinterfaceOptions{
  min?:number
  max?:number}exporttypeValueParam=number|((c:number)=>number)functionuseCounter(initialValue =0){const[current, setCurrent]=useState(initialValue)constsetValue=(value: ValueParam)=>{setCurrent((preValue)=>(typeof value ==='number'? value :value(preValue)))}// 增加constincrease=(delta =1)=>{setValue((preValue)=> preValue + delta)}// 减少constdecrease=(delta =1)=>{setValue((preValue)=> preValue - delta)}// 设置指定值constspecifyValue=(value: ValueParam)=>{setValue(value)}// 重置值constresetValue=()=>{setValue(initialValue)}return[
    current,{
      increase,
      decrease,
      specifyValue,
      resetValue,},]asconst}exportdefault useCounter

🙋有些同学会觉得 Hook 不就是纯函数么?为什么不能直接像纯函数那样去测呢?
❌ NoNoNo,React 规定 只有在组件中才能使用这些 Hooks,所以这样测试的结果就会得到下面的报错:
在这里插入图片描述

🙋那又有同学问了,我直接 Mock 掉这些 Hook 不就解决了?
❌ NoNoNo,假如除了 useState,还有 useEffect 这样的呢? 难道每个 React API 都要 Mock 一遍吗?

👉 这里循序渐进列举了三种方法,更推荐第三种哦~

1. 写组件进行整体测试

首先写一个组件,然后在组件内使用 useCounter,并把增加、减少、设置和重置功能绑定到按钮:

import React from'react'import useCounter from'./useCounter'exportconstUseCounterTest=()=>{const[counter,{ increase, decrease, specifyValue, resetValue }]=useCounter(0)return(<section><div>Counter:{counter}</div><button onClick={()=>increase(1)}>点一下加一</button><button onClick={()=>decrease(1)}>点一下减一</button><button onClick={()=>specifyValue(10)}>点一下变成十</button><button onClick={resetValue}>重置</button></section>)}

在每个用例中,我们通过点击按钮来模拟函数的调用,最后 expect 一下 Counter:n 的文本结果来完成测试:

import React from'react'import{ describe, expect }from'@jest/globals'import{ render, fireEvent }from'@testing-library/react'import'@testing-library/jest-dom/extend-expect'import{ UseCounterTest }from'.'describe('useCounter',()=>{it('可以做加法',async()=>{const{ getByText }=render(<UseCounterTest />)
    fireEvent.click(getByText('点一下加一'))expect(getByText('Counter: 1')).toBeInTheDocument()})it('可以做减法',async()=>{const{ getByText }=render(<UseCounterTest />)
    fireEvent.click(getByText('点一下减一'))expect(getByText('Counter: -1')).toBeInTheDocument()})it('可以设置值',async()=>{const{ getByText }=render(<UseCounterTest />)
    fireEvent.click(getByText('点一下变成十'))expect(getByText('Counter: 10')).toBeInTheDocument()})it('可以重置值',async()=>{const{ getByText }=render(<UseCounterTest />)
    fireEvent.click(getByText('点一下变成十'))
    fireEvent.click(getByText('重置'))expect(getByText('Counter: 0')).toBeInTheDocument()})})

这个方法并不好,因为要用按钮来绑定一些操作并触发,可不可以直接操作函数呢?

2. 创建 setup 函数进行测试

我们不想一直和组件进行交互做测试,那么这个方法则只是借了 组件环境来生成一下 useCounter 结果, 用完就把别人抛弃了。

import React from'react'import{ act, render }from'@testing-library/react'import useCounter,{ ValueParam }from'../useCounter'interfaceUseCounterData{
  counter:number
  utils:{increase:(delta?:number)=>voiddecrease:(delta?:number)=>voidspecifyValue:(value: ValueParam)=>voidresetValue:()=>void}}constsetup=(initialNumber:number)=>{const returnVal ={}as UseCounterData
  constUseCounterTest=()=>{const[counter, utils]=useCounter(initialNumber)
    Object.assign(returnVal,{
      counter,
      utils,})returnnull}render(<UseCounterTest />)return returnVal
}describe('useCounter',()=>{it('可以做加法',async()=>{const useCounterData: UseCounterData =setup(0)act(()=>{
      useCounterData.utils.increase(1)})expect(useCounterData.counter).toEqual(1)})it('可以做减法',async()=>{const useCounterData: UseCounterData =setup(0)act(()=>{
      useCounterData.utils.decrease(1)})expect(useCounterData.counter).toEqual(-1)})it('可以设置值',async()=>{const useCounterData: UseCounterData =setup(0)act(()=>{
      useCounterData.utils.specifyValue(10)})expect(useCounterData.counter).toEqual(10)})it('可以重置值',async()=>{const useCounterData: UseCounterData =setup(0)act(()=>{
      useCounterData.utils.specifyValue(10)
      useCounterData.utils.resetValue()})expect(useCounterData.counter).toEqual(0)})})

注意:由于setState 是一个异步逻辑,因此我们可以使用 @testing-library/react 提供的 act 里调用它。
act 可以确保回调里的异步逻辑走完再执行后续代码,详情可见官网这里

3. 使用 renderHook 测试
基于这样的想法,@testing-library/react-hooks 把上面的步骤封装成了一个公共函数 renderHook

注意:在 @testing-library/react@13.1.0 以上的版本已经把 renderHook 内置到里面了,这个版本需要和
react@18 一起使用。如果是旧版本,需要单独下载 @testing-library/react-hooks 包。

这里我使用新的版本,也就是内置的 renderHook:

import{ act, renderHook }from'@testing-library/react'import useCounter from'../useCounter'describe('useCounter',()=>{it('可以做加法',()=>{const{ result }=renderHook(()=>useCounter(0))act(()=>{
      result.current[1].increase(1)})expect(result.current[0]).toEqual(1)})it('可以做减法',()=>{const{ result }=renderHook(()=>useCounter(0))act(()=>{
      result.current[1].decrease(1)})expect(result.current[0]).toEqual(-1)})it('可以设置值',()=>{const{ result }=renderHook(()=>useCounter(0))act(()=>{
      result.current[1].specifyValue(10)})expect(result.current[0]).toEqual(10)})it('可以重置值',()=>{const{ result }=renderHook(()=>useCounter(0))act(()=>{
      result.current[1].specifyValue(10)
      result.current[1].resetValue()})expect(result.current[0]).toEqual(0)})})

实际上 renderHook 只是 setup 方法里 setupTestComponent 的高度封装而已。

💥 其他的疑难杂症

如果测试组件和 React Router 做交互:

// useQuery.tsimport React from'react'import{ useLocation }from'react-router-dom'// 获取查询参数exportconstuseQuery=()=>{const{ search }=useLocation()return React.useMemo(()=>newURLSearchParams(search),[search])}// index.tsximport React from'react'import{ useQuery }from'../useQuery'exportconstMyComponent=()=>{const query =useQuery()return<div>{query.get('id')}</div>}

使用 useLocation 时报错:
在这里插入图片描述

要创建 React Router 环境,我们可以使用 createMemoryHistory 这个 API:

import React from'react'import{ useQuery }from'../useQuery'import{ createMemoryHistory, InitialEntry }from'history'import{ render }from'@testing-library/react'import{ Router }from'react-router-dom'constsetup=(initialEntries: InitialEntry[])=>{const history =createMemoryHistory({
    initialEntries,})const returnVal ={
    query:newURLSearchParams(),}constTestComponent=()=>{const query =useQuery()
    Object.assign(returnVal,{ query })returnnull}// 此处为 react router v6 的写法render(<Router location={history.location} navigator={history}><TestComponent /></Router>)// 此处为 react router v5 的写法// render(//   <Router history={history}>//     <TestComponent />//   </Router>// );return returnVal
}describe('userQuery',()=>{it('可以获取参数',()=>{const result =setup([{
        pathname:'/home',
        search:'?id=123',},])expect(result.query.get('id')).toEqual('123')})it('查询参数为空时返回 Null',()=>{const result =setup([{
        pathname:'/home',},])expect(result.query.get('id')).toBeNull()})})

另:好用的方法 🌟

1. test.only
使用场景:只想对单个测试用例进行调试时
在同一测试文件中,只有使用test.only的测试用例会被执行,其他测试用例则会被跳过。
举个例子🌰:(只有第二个测试用例会运行,第一个会被跳过,

其他文件中的测试用例不会被跳过

describe('Example',()=>{test('随便不知道是啥',()=>{// 测试用例})
  test.only('我就举个例子',()=>{// 测试用例})})

2. test.skip
使用场景:想跳过某个测试用例进行调试时
在同一测试文件中,使用test.skip的测试用例会被跳过,其他测试用例正常执行。
用法同 test.only 我就不写例子了

还有好用的我再补充,散会~ 👏

标签: 单元测试 Jest

本文转载自: https://blog.csdn.net/fish_skyyyy/article/details/137468111
版权归原作者 蛞蝓不孤寡 所有, 如有侵权,请联系我们删除。

“jest单元测试——项目实战”的评论:

还没有评论