jest单元测试——项目实战

news2025/1/24 4:57:12

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: string
  showStar?: boolean
}

const Prefix = 'card-title'
export const CardTitle = (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.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?: number
  max?: 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: number
  utils: {
    increase: (delta?: number) => void
    decrease: (delta?: number) => void
    specifyValue: (value: ValueParam) => void
    resetValue: () => void
  }
}

const setup = (initialNumber: number) => {
  const returnVal = {} as UseCounterData
  const 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.coloradmin.cn/o/1576037.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2-django、http、web框架、django及django请求生命周期、路由控制、视图层

1 http 2 web框架 3 django 3.1 django请求生命周期 4 路由控制 5 视图层 1 http #1 http 是什么 #2 http特点 #3 请求协议详情-请求首行---》请求方式&#xff0c;请求地址&#xff0c;请求协议版本-请求头---》key:value形式-referer&#xff1a;上一次访问的地址-user-agen…

特别详细的Spring Cloud 系列教程1:服务注册中心Eureka的启动

Eureka已经被Spring Cloud继承在其子项目spring-cloud-netflix中&#xff0c;搭建Eureka Server的方式还是非常简单的。只需要通过一个独立的maven工程即可搭建Eureka Server。 我们引入spring cloud的依赖和eureka的依赖。 <dependencyManagement><!-- spring clo…

软件无线电系列——CIC滤波器

本节目录 一、CIC滤波器 1、积分器 2、梳状滤波器 3、CIC滤波器的特性 二、单级CIC和多级CIC滤波器本节内容 一、CIC滤波器 CIC滤波器&#xff0c;英文名称为Cascade Integrator Comb&#xff0c;中文全称为级联积分梳状滤波器&#xff0c;从字面来看就是将积分器与梳状滤波器…

VS Code远程连接服务器运行python程序

之前一直用pycharm连接服务器跑程序&#xff0c;pycharm需要本地和远程都存一份代码&#xff0c;然后把本地的更新同步到服务器上来实现代码修改&#xff0c;后来实习的时候发现企业里面都用VS Code&#xff0c;不得不说&#xff0c;VS Code真的很方便&#xff0c;直接连服务器…

持续交付工具Argo CD的部署使用

Background CI/CD&#xff08;Continuous Integration/Continuous Deployment&#xff09;是一种软件开发流程&#xff0c;旨在通过自动化和持续集成的方式提高软件交付的效率和质量。它包括持续集成&#xff08;CI&#xff09;和持续部署&#xff08;CD&#xff09;两个主要阶…

解决前端精度丢失问题:后端Long类型到前端的处理策略

在Web开发中&#xff0c;我们经常遇到前后端数据类型不匹配的问题&#xff0c;特别是当后端使用大数据类型如Long时&#xff0c;前端由于JavaScript的数字精度限制&#xff0c;可能导致精度丢失。本文将深入探讨这个问题&#xff0c;并提供两种有效的解决方法。 一、问题背景 …

Python学习从0开始——007输入与输出

Python学习从0开始——007输入与输出 一、简单输出二、复杂输出2.1引用变量2.2format()函数2.3手动格式化 三、读写文件3.1open()3.2操作文件对象3.3使用 json 保存结构化数据 一、简单输出 str() 函数返回供人阅读的值&#xff0c;repr() 则生成适于解释器读取的值&#xff0…

加州大学欧文分校英语基础语法专项课程01:Word Forms and Simple Present Tense 学习笔记

Word Forms and Simple Present Tense Course Certificate 本文是学习Coursera上 Word Forms and Simple Present Tense 这门课程的学习笔记。 文章目录 Word Forms and Simple Present TenseWeek 01: Introduction & BE VerbLearning Objectives Word FormsWord Forms (P…

【AI】ubuntu 22.04 本地搭建Qwen-VL 支持图片识别的大语言模型 AI视觉

下载源代码 yeqiangyeqiang-MS-7B23:~/Downloads/src$ git clone https://gh-proxy.com/https://github.com/QwenLM/Qwen-VL 正克隆到 Qwen-VL... remote: Enumerating objects: 584, done. remote: Counting objects: 100% (305/305), done. remote: Compressing objects: 10…

Java单链表和LinkedList的实现

一、单链表的实现 无头单向非循环链表 定义异常用于判断所给位置是否合法 public class IndexNotLegal extends RuntimeException{public IndexNotLegal(){}public IndexNotLegal(String smg){super(smg);} } class ListNode中包含当前节点的值和下一个节点指向 实现链表的…

摄影杂记二

一、相机操作指南 ⑴按键说明&#xff1a; 除了常规的几个模式&#xff0c;里面就特殊场景可以看一下&#xff0c;有全景&#xff0c;支持摇摄。 lock&#xff1a;多功能锁。可以锁定控制按钮和控制环。在设置中找到多功能锁&#xff0c;可以设置锁定什么。 m-fn&#xff1a;多…

自定义校验器

1.前端校验 <template><el-dialog:title"!dataForm.brandId ? 新增 : 修改":close-on-click-modal"false":visible.sync"visible"><el-form:model"dataForm":rules"dataRule"ref"dataForm"keyu…

解决方案AssertionError: Torch not compiled with CUDA enabled

文章目录 一、现象二、解决方案 一、现象 报错显示 s torch.from_numpy(padding_seq([s])).cuda().long() File "D:\Anaconda3\lib\site-packages\torch\cuda\__init__.py", line 221, in _lazy_initraise AssertionError("Torch not compiled with CUDA ena…

基于微信小程序的亿家旺生鲜云订单零售系统的设计与实现(论文+源码)_kaic

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了亿家旺生鲜云订单零售系统的开发全过程。通过分析亿家旺生鲜云订单零售系统管理的不足&#xff0c;创建了一个计算机管理亿家旺生鲜云订单零售系统的方案。文章介…

Text-Driven Object Detection 关于结合文本的目标检测

1、简单介绍 首先说明&#xff0c;本文目的主要是水一篇CSDN博客&#xff0c;顺便说一下和标题相关的认识。 近几年&#xff0c;在目标检测领域关于多模态的目标检测工作已成了主流&#xff0c;趋势仍在延续&#xff0c;未来仍有很大挖掘空间。这里说的多模态不是简单的多源数…

Sql优化篇-干货总结大全

前言 我们经常会听到Sql优化的一个概念&#xff0c;但其实sql优化不一定就是说sql语句写的有问题&#xff0c;它可能是因为cpu资源爆满&#xff0c;或者内存空间不足&#xff0c;它也会导致sql执行卡顿&#xff1b;或者说表设计层面&#xff0c;过滤条件没有加索引之类的 等等…

【STL】list的底层原理及其实现

文章目录 list的介绍list的整体结构设计list的构造代码模拟实现&#xff1a; list节点类的实现list 迭代器Iterator的使用以及实现Iterator的使用Iterator的底层实现反向迭代器 list与vector的比较实现list类 list的介绍 list是可以在常数范围内在任意位置进行插入和删除的序列…

springCloud-LoadBalancer负载均衡微服务负载均衡器LoadBalancer

2020年前SpringCloud是采用Ribbon作为负载均衡实现&#xff0c;但是在2020后采用了LoadBalancer替代 LoadBalancer默认提供了两种负载均衡策略&#xff08;只能通过配置类来修改负载均衡策略&#xff09; 1.RandomLoadBalancer-随机分配策略 2.RoundRobinLoadBalancer-轮询分配…

【WSN覆盖优化】基于灰狼优化算法的无线传感器网络覆盖 GWO-WSN覆盖优化【Matlab代码#74】

文章目录 【可更换其他算法&#xff0c;获取资源请见文章第5节&#xff1a;资源获取】1. 灰狼优化算法2. WSN节点感知模型3. 部分代码展示4. 仿真结果展示5. 资源获取 【可更换其他算法&#xff0c;获取资源请见文章第5节&#xff1a;资源获取】 1. 灰狼优化算法 此处略。 2.…

尚硅谷html5+css3(1)html相关知识

1.基本标签&#xff1a; <h1>最大的标题字号 <h2>二号标题字号 <p>换行 2.根标签<html> 包括<head>和<body> <html><head><title>title</title><body>body</body></head> </html> 3…