jest单元测试——项目实战

jest单元测试——项目实战

  • 一、纯函数测试
  • 二、组件测试
  • 三、接口测试
  • 四、React Hook测试
  • 💥 其他的疑难杂症
  • 另:好用的方法 🌟

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

一、纯函数测试

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

// demo.ts
/*** 比较两个数组内容是否相同* @param {Array} arr1 - 第一个数组* @param {Array} arr2 - 第二个数组* @returns {Boolean} - 如果两个数组内容相同,返回 true,否则返回 false*/
export const compareArrays = (arr1: ReactText[], arr2: ReactText[]) => {if (arr1.length !== arr2.length) {return false} else {const result = arr1.every((item) => arr2.includes(item))return result}
}//demo.test.ts
describe('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.tsx
import React from 'react'
import './index.scss'interface Props {title: stringshowStar?: boolean
}const Prefix = 'card-title'
export const CardTitle = (props: Props) => {const { title, showStar = true } = propsreturn (<div className={`${Prefix}-title`}>{showStar && <span className={`${Prefix}-title-star`}>*</span>}<div>{title}</div></div>)
}// demo.test.tsx
import 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(接口)
export const getUserRole = async () => {const result = await axios.post('XXX', { data: 'abc' })return result.data
}
// index.ts(调用函数)
export const getUserType = async () => {const result = await getUserRole()return result
}

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

it('mock axios', async () => {jest.spyOn(axios, 'post').mockResolvedValueOnce({data: { userType: 'user' },})const { userType } = await getUserType()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 } = await getUserType()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()const setup = (data: { userType: string }) => {server.use(rest.post(url, async (req, res, ctx) => {return res(ctx.status(200), ctx.json(data))}))}beforeAll(() => {server.listen()})afterEach(() => {server.resetHandlers()})afterAll(() => {server.close()})it('mock http', async () => {setup({ userType: 'user' })const { userType } = await getUserType()expect(userType).toBe('user')})
})

四、React Hook测试

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

import { useState } from 'react'export interface Options {min?: numbermax?: number
}export type ValueParam = number | ((c: number) => number)function useCounter(initialValue = 0) {const [current, setCurrent] = useState(initialValue)const setValue = (value: ValueParam) => {setCurrent((preValue) => (typeof value === 'number' ? value : value(preValue)))}// 增加const increase = (delta = 1) => {setValue((preValue) => preValue + delta)}// 减少const decrease = (delta = 1) => {setValue((preValue) => preValue - delta)}// 设置指定值const specifyValue = (value: ValueParam) => {setValue(value)}// 重置值const resetValue = () => {setValue(initialValue)}return [current,{increase,decrease,specifyValue,resetValue,},] as const
}export default useCounter

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

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

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

1. 写组件进行整体测试

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

import React from 'react'
import useCounter from './useCounter'export const UseCounterTest = () => {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'interface UseCounterData {counter: numberutils: {increase: (delta?: number) => voiddecrease: (delta?: number) => voidspecifyValue: (value: ValueParam) => voidresetValue: () => void}
}const setup = (initialNumber: number) => {const returnVal = {} as UseCounterDataconst UseCounterTest = () => {const [counter, utils] = useCounter(initialNumber)Object.assign(returnVal, {counter,utils,})return null}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.ts
import React from 'react'
import { useLocation } from 'react-router-dom'// 获取查询参数
export const useQuery = () => {const { search } = useLocation()return React.useMemo(() => new URLSearchParams(search), [search])
}// index.tsx
import React from 'react'
import { useQuery } from '../useQuery'export const MyComponent = () => {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'const setup = (initialEntries: InitialEntry[]) => {const history = createMemoryHistory({initialEntries,})const returnVal = {query: new URLSearchParams(),}const TestComponent = () => {const query = useQuery()Object.assign(returnVal, { query })return null}// 此处为 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 我就不写例子了

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/301307.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法刷题Day24 | 回溯算法基础理论、 77. 组合

目录 0 引言1 回溯算法基础理论1.1 回溯算法模板1.2 2 组合2.1 我的解题2.2 剪枝操作 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;算法专栏&#x1f4a5; 标题&#xff1a;算法刷题Day23 | 回溯算法基础理论、 77. 组合❣️ 寄语&#xff1a;书…

2024年第七届信息管理与管理科学国际会议(IMMS 2024)即将召开!

2024年第七届信息管理与管理科学国际会议&#xff08;IMMS 2024&#xff09;将于2024年8月23-25日在中国北京举行。数字化时代&#xff0c;我们面临着诸多挑战&#xff0c;如信息安全问题、数据治理难题、管理创新需求等。IMMS 2024的召开&#xff0c;旨在让全球信息管理与管理…

Centos7 安装GitLab

安装环境: 虚拟机:Centos7 最小安装 4核8G 下载GitLab 本次实验下载的是 gitlab-ce-14.1.0-ce.0.el7.x86_64.rpm 官网截图 清华源截图 安装包下载地址(官网;下载CE版本,EE是收费版本):https://packages.gitlab.com/gitlab/gitlab-ce国内镜像源下载地址(清华源):htt…

(源码+部署+讲解)基于Spring Boot和Vue的大学志愿者服务平台的设计与实现

摘要&#xff1a; 随着互联网技术的快速发展&#xff0c;大学校园内的志愿者活动日益增多&#xff0c;传统的志愿者管理方式已难以满足现代化、信息化的需求。因此&#xff0c;设计并实现一个基于Spring Boot和Vue的大学志愿者服务平台显得尤为重要。本文详细阐述了该平台的设计…

前端三剑客 —— CSS (第五节)

目录 内容回顾&#xff1a; 特殊样式 特殊样式 CSS变量 常见函数 倒影效果 页面布局 Table 布局&#xff08;了解即可&#xff09; DIVCSS布局 弹性布局 1&#xff09;不使用弹性布局&#xff0c;而是使用DIVCSS 2&#xff09;使用弹性布局实现导航菜单 内容回顾…

Windows深度学习环境----Cuda version 10.2 pytorch3d version 0.3.0

Requirements Python version 3.8.5Pytorch version: pytorch1.6.0 torchvision0.8.2 torchaudio0.7.0 cudatoolkit10.2.89pytorch3d version 0.3.0Cuda version 10.2 感觉readme文件里的不适配&#xff0c;跟pytorch官网不同 以前的 PyTorch 版本 |PyTorch的 # CUDA 10.2 c…

睿考网:小白怎么准备二级建造师考试?

小白想要准备二级建造师考试&#xff0c;可以遵循以下策略&#xff1a; 1.定位明确&#xff0c;设定目标&#xff0c;确保三门科目达到及格标准&#xff0c;避免学科偏重。 2.基础知识扎实&#xff0c;考试内容主要来自教材&#xff0c;因此&#xff0c;理解和记忆所学的基础…

Redis: 持久化

文章目录 一、RDB持久化1、概念2、生成、载入RDB文件3、执行时机&#xff08;1&#xff09; 执行save命令&#xff08;2&#xff09;执行bgsave命令&#xff08;3&#xff09;Redis停机时&#xff08;4&#xff09;触发RDB条件 4、bgsave原理5、小结 二、AOF持久化1、概念2、AO…

Linux初学(十四)LampLnmp

一、简介 LAMP和LNMP是两种常见的web服务器组合。具体如下&#xff1a; LAMP&#xff1a;LAMP代表的是Linux&#xff08;操作系统&#xff09; Apache&#xff08;HTTP服务器&#xff09; MySQL&#xff08;数据库&#xff09; PHP&#xff08;编程语言&#xff09;。这个组合被…

微信小程序 电影院售票选座票务系统5w7l6

uni-app框架&#xff1a;使用Vue.js开发跨平台应用的前端框架&#xff0c;编写一套代码&#xff0c;可编译到Android、小程序等平台。 框架支持:springboot/Ssm/thinkphp/django/flask/express均支持 前端开发:vue.js 可选语言&#xff1a;pythonjavanode.jsphp均支持 运行软件…

OpenCV单通道图像按像素成倍比例放大(无高斯平滑处理)

OpenCV中的resize函数可以对图像做任意比例的放大(/缩小)处理&#xff0c;该处理过程会对图像做高斯模糊化以保证图像在进行放大&#xff08;/缩小&#xff09;后尽可能保留源图像所展现的具体内容&#xff08;消除固定频率插值/采样带来的香农采样信息损失&#xff09;&#x…

TCP客户端及服务器端开发实践

一、TCP客户端及服务器端开发实践 1、TCP网络应用程序开发分类 ① TCP客户端应用程序开发 ② TCP服务器端应用程序开发 客户端程序是指运行在用户设备上的程序&#xff0c;服务端程序是指运行在服务器设备上的程序&#xff0c;专门为客户端提供数据服务。那如何记忆呢&…

Centos7安装jdk

下载上传并解压 下载 jdk-8u201-linux-x64.tar.gz 链接&#xff1a;https://pan.baidu.com/s/13WWt6ArVYXt8QmdU3Z3zOg?pwdwxyu 提取码&#xff1a;wxyu 上传 上传到服务器/opt目录 解压 cd /opt tar -zxvf jdk-8u201-linux-x64.tar.gz 配置环境变量 vi /etc/profil…

Vuex状态管理

1.什么是状态管理 在开发中&#xff0c;我们会让应用程序需要处理各种各样的数据&#xff0c;这些数据需要保存在我们应用程序中的某一个位置&#xff0c;对于这些数据的管理我们就 称之为是状态管理。 在Vue开发中&#xff0c;我们使用组件化的开发方式: 1.在组件中我们定义…

【相机方案】智能驾驶的域控采用的“串行器和解串器”方案的总结(持续更新),SerDes,GMSL

SerDes是Serializer/Deserializer的缩写&#xff0c;即串行器和解串器。由于同轴线的传输延迟几乎可以忽略不计&#xff08;ns级别&#xff09;&#xff0c;相当于将原来只能短距离传输的高速并行信号(MIPI/I2C/CLK等)的传输距离延长&#xff0c;真正做到高带宽、低延迟、长距离…

蓝桥杯刷题--RDay5

清理水域--枚举 8.清理水域 - 蓝桥云课 (lanqiao.cn)https://www.lanqiao.cn/problems/2413/learning/?page1&first_category_id1&second_category_id3&tags2023 小蓝有一个n m大小的矩形水域&#xff0c;小蓝将这个水域划分为n行m列&#xff0c;行数从1…

【QT+QGIS跨平台编译】056:【pdal_kazhdan+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

点击查看专栏目录 文章目录 一、pdal_kazhdan介绍二、pdal下载三、文件分析四、pro文件五、编译实践一、pdal_kazhdan介绍 pdal_kazhdan 是 PDAL(Point Data Abstraction Library)相关的 Kazhdan 算法的实现。PDAL 是一个用于处理和分析点云数据的开源库,而 Kazhdan 算法通常…

AI提速 OpenAI 新模型GPT-5今年上线?

这两天&#xff0c;有关OpenAI新模型 GPT-5的消息又多了起来。有知情人士称&#xff0c;OpenAI将在今年年中的某个时候发布GPT-5&#xff0c;很可能是在今年夏天期间。OpenAI CEO 萨姆奥特曼在一次播客采访中透露“GPT-5的智能水平得到提升”。 有趣的是&#xff0c;播客的主理…

Latex表格制作详细教程(table, tabular, multirow, multicolumn)

一、简单表格制作 Latex表格需要用到 table 和 tabular 环境。其中 table 环境里写表格的标题(caption&#xff09;、表格的位置之类的。 tabular 环境则是绘制表格的内容。一个简单的表格绘制代码如下所示&#xff1a; \documentclass{article}\begin{document}\begin{table…

三防平板定制服务:亿道信息与个性化生产的紧密结合

在当今数字化时代&#xff0c;个性化定制已经成为了市场的一大趋势&#xff0c;而三防平板定制服务作为其中的一部分&#xff0c;展现了数字化技术与个性化需求之间的紧密结合。这种服务是通过亿道信息所提供的技术支持&#xff0c;为用户提供了满足特定需求的定制化三防平板&a…