通过Jest实现react组件和自定义hooks的单元测试

2,671 阅读7分钟

1、为什么组件要写单元测试

1、减少bug: 一个可单元测试的工程,会把业务、功能分割成规模更小、有独立的组件。单元测试的目标,就是保证各个组件的逻辑正确性。从而保证整个“机器”(项目)运行正确,最大限度减少bug。

2、提高代码质量: 由于每个组件都有独立的逻辑,做单元测试时需要隔离外部依赖,确保这些依赖不影响验证逻辑。因为要把各种依赖分离,单元测试会促进工程进行组件拆分,对组件进行更好的设计,更大程度减少代码耦合。这样写出来的代码,更好维护,更好扩展,从而提高代码质量。

3、快速定位bug、减少调试时间: 如果程序有bug,我们运行一次全部单元测试,找到不通过的测试,可以很快地定位对应的执行代码。修复代码后,运行对应的单元测试;如还不通过,继续修改,运行测试.....直到测试通过。

4、形成文档: 单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步,代码也便于维护和理解。


测试驱动开发, 是敏捷开发的一项核心实践和技术,也是一种设计方法论。TDD原理是开发功能代码之前,先编写测试用例代码,然后针对测试用例编写功能代码,使其能够通过。由于TDD对开发人员要求非常高,跟传统开发思维不一样,因此实施起来相当困难。测试驱动开发有好处也有坏处。因为每个测试用例都是根据需求来的,或者说把一个大需求分解成若干小需求编写测试用例,所以测试用例写出来后,开发者写的执行代码,必须满足测试用例。如果测试不通过,则修改执行代码,直到测试用例通过。**好处:**通过测试的执行代码,肯定满足需求,而且有助于接口编程,降低代码耦合,也极大降低bug出现几率(如果是极限编程,几乎是不可能有bug)。**坏处:**1.投入开发资源(时间和精力);2.由于测试用例在未进行代码设计前写;很有可能限制开发者对代码整体设计;3.可能引起开发人员不满情绪,我觉得这点很严重,毕竟不是人人都喜欢单元测试,尽管单元测试会带给我们相当多的好处。


2、利用Jest测试React组件

测试框架 Jest

关于Jest,我们参考一下其Jest官网

Jest于2014 年首次发布,虽然它最初引起了很多人的兴趣,但该项目一度处于休眠状态。然而,Facebook 投入了大量精力来改进 Jest,随后发布了一些更新的版本,与最初的开源版本相比,Jest 的唯一相似之处是名称和徽标,其他一切都已更改和重写

jest使用

$ npm install jest --save-dev
# 
$ yarn add jest --dev

jest.config.js

  • 可以运行npx jest --init在根目录生成配置文件jest.config.js
import type { Config } from '@jest/types'

const config: Config.InitialOptions = {
  clearMocks: true,
  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
  coverageDirectory: 'coverage',
  moduleDirectories: ['node_modules', 'src'],
  moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
  bail: true,
  setupFilesAfterEnv: ['@testing-library/jest-dom'],
  testEnvironment: 'jsdom',
  testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
  transformIgnorePatterns: ['/node_modules/'],
  preset: 'ts-jest',
}

module.exports = config

这里只是列举了常用的配置项 :

  • automock: 告诉 Jest 所有的模块都自动从 mock 导入
  • clearMocks: 在每个测试前自动清理 mock 的调用和实例 instance
  • collectCoverageFrom: 生成测试覆盖报告时检测的覆盖文件
  • coverageDirectory: Jest 输出覆盖信息文件的目录
  • coverageReporters: 列出包含 reporter 名字的列表,而 Jest 会用他们来生成覆盖报告
  • coverageThreshold: 测试可以允许通过的阈值
  • moduleDirectories: 模块搜索路径
  • moduleFileExtensions: 代表支持加载的文件名
  • testPathIgnorePatterns: 用正则来匹配不用测试的文件
  • setupFilesAfterEnv: 配置文件,在运行测试案例代码之前,Jest 会先运行这里的配置文件来初始化指定的测试环境
  • testMatch: 定义被测试的文件
  • transformIgnorePatterns: 设置哪些文件不需要转译
  • transform: 设置哪些文件中的代码是需要被相应的转译器转换成 Jest 能识别的代码,Jest 默认是能识别 JS 代码的,其他语言,例如 Typescript、CSS 等都需要被转译。

具体配置在这里

在项目package.json文件添加如下script:

  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "test": "jest",
    "testa": "jest --watchAll"
  }
  • 执行yarn test运行配置中所有的测试

  • 执行yarn testa 会分以下几种模式

  • f: 只会测试之前没有通过的测试用例

  • o: 只会测试关联的并且改变的文件(需要使用 git)(jest --watch 可以直接进入该模式)

  • p: 测试文件名包含输入的名称的测试用例

  • t: 测试用例的名称包含输入的名称的测试用例

  • a: 运行全部测试用例

Jest匹配器

  • toBe(value): 使用 Object.is 来进行比较,如果进行浮点数的比较,要使用 toBeCloseTo
  • not: 取反
  • toEqual(value): 用于对象的深比较
  • toContain(item): 用来判断 item 是否在一个数组中,也可以用于字符串的判断
  • toBeNull(value): 只匹配 null
  • toBeUndefined(value): 只匹配 undefined
  • toBeDefined(value): 与 toBeUndefined 相反
  • toBeTruthy(value): 匹配任何语句为真的值
  • toBeFalsy(value): 匹配任何语句为假的值
  • toBeGreaterThan(number): 大于
  • toBeGreaterThanOrEqual(number): 大于等于
  • toBeLessThan(number): 小于
  • toBeLessThanOrEqual(number): 小于等于
  • toBeInstanceOf(class): 判断是不是 class 的实例
  • resolves: 用来取出 promise 为 fulfilled 时包裹的值,支持链式调用
  • rejects: 用来取出 promise 为 rejected 时包裹的值,支持链式调用
  • toHaveBeenCalled(): 用来判断 mock function 是否被调用过
  • toHaveBeenCalledTimes(number): 用来判断 mock function 被调用的次数
  • assertions(number): 验证在一个测试用例中有 number 个断言被调用

Jest匹配器列表

注意,Jest 并不是专门针对 React 的测试框架,你可以使用它来测试任何 JavaScript 应用程序。然而,它提供的一些特性对于测试用户界面非常方便,这就是它非常适合 React 的原因。

React测试库react-testing-library

在React中常见的测试库有2个,一个是Enzyme,一个是react-testing-library。虽然Enzymegithub上star多一些,但是近期下载量来看react-testing-library更高一点,并且react-testing-libraryReact v17,v18的兼容性也会更好一些,而在使用Enzyme同时,我们还要为我们使用的React不同版本安装适配器,v15,v16有官方适配器,v17,v18目前还没有官方适配器可用,只能用社区版本的,所以本文选择使用react-testing-library

react-testing-library 安装

虽然它的名字叫React Testing Library,但是它的包名叫@testing-library/react。

$ npm install --save-dev @testing-library/react @testing-library/jest-dom
# 
$ yarn add --dev @testing-library/react @testing-library/jest-dom
  • @testing-library/jest-dom添加了一些额外的匹配器,用来测试dom

  • 需要将它加入jest的配置,同时将jest环境设置为jsdom

  • @testing-library/jest-dom加到jest.config.js

module.exports = {
  setupFilesAfterEnv: ["@testing-library/jest-dom"],
  testEnvironment: "jsdom",
};

hook单元测试

react-hooks-testing-library允许您为 React 钩子创建一个简单的测试工具,以处理在函数组件的主体内运行它们,并提供各种有用的实用函数来更新输入和检索令人惊叹的自定义钩子的输出。该库旨在提供尽可能接近在真实组件中本地使用您的钩子的测试体验。

使用这个库,您不必担心如何构建、渲染或与 react 组件交互来测试您的钩子。您可以直接使用钩子并断言结果。

安装

$ npm install --save-dev @testing-library/react-hooks

测试用例

useCounter.js

import { useState, useCallback } from 'react'

function useCounter() {
  const [count, setCount] = useState(0)

  const increment = useCallback(() => setCount((x) => x + 1), [])

  return { count, increment }
}

export default useCounter

useCounter.test.js

import { renderHook, act } from '@testing-library/react-hooks'
import useCounter from './useCounter'

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter())

  act(() => {
    result.current.increment()
  })

  expect(result.current.count).toBe(1)
})

表单组件单元测试

/**
 * @jest-environment jsdom
 */
import * as React from 'react'
import axios from 'axios'
import '@testing-library/jest-dom/extend-expect'
import { render, screen, fireEvent } from '@testing-library/react'
import Form from '../../src/components/form'

jest.mock('axios')

// 简单校验input失去焦点是如果没有值是不是会提示'用户名不可以为空'的信息
test('form表单用户名验空', () => {
	// 渲染表单组件
  render(<Form />)
	// 获取dom
  const name = screen.getByTestId('name')
	// 模拟input失去焦点事件
  fireEvent.blur(name)
	// 获取抱错信息dom
  const nameErrorMsg = screen.getByTestId('nameErrorMsg')
	// 断言 抱错信息dom的文案是不是 '用户名不可以为空'
  expect(nameErrorMsg).toHaveTextContent('用户名不可以为空')
})

// 通过`axios`mock提交表单的接口返回数据
test('form表单提交数据', async () => {
// mock数据 将入参name, password 拼接成token字段返回 组件中将返回的token存储到localStorage中
  axios.post.mockImplementationOnce((url, body) => {
    const { name, password } = body?.data
    const token = `token=${name}+${password}`
    return Promise.resolve({ data: token })
  })
	// 渲染组件
  render(<Form />)
	// 将`admin`赋值到用户名字段
  const name = screen.getByTestId('name')
  fireEvent.change(name, { target: { value: 'admin' } })
	// 将`123456`赋值到密码字段
  const password = screen.getByTestId('password')
  fireEvent.change(password, { target: { value: '123456' } })
	// 获取提交按钮
  const submit = screen.getByTestId('submit')
	// 模拟按钮点击事件 异步提交数据
  await fireEvent.click(submit)
	// 断言localStorage中`_token`字段是否有值
  expect(window.localStorage.getItem('_token')).not.toBeNull()
})

总结

以上就是自己总结的react的组件和自定义hooks的单元测试,希望对你有帮助,欢迎评论区交流讨论!