• (2.2w字)前端单元测试之Jest详解篇


    Jest

    Jest 概述

    Jest是一个领先的JavaScript测试框架,特别适用于React和Node.js环境。由Facebook开发,它以简单的配置、高效的性能和易用性而闻名。Jest支持多种类型的测试,包括单元测试、集成测试和快照测试,后者用于捕获组件或数据结构的状态,以便于后续的比较和验证。Jest自动化模拟依赖项和异步代码测试,提高了测试的可靠性和灵活性。其并行测试执行机制显著加快了测试过程,而交互式监视模式则在开发过程中提供即时反馈。此外,Jest还提供内置的代码覆盖率工具,帮助开发者优化测试范围。因其强大的功能和广泛的社区支持,Jest成为现代JavaScript项目中不可或缺的测试工具。

    Jest 环境配置

    安装包

    1、jest:这是 Jest 测试框架本身。

    2、@types/jest:这是 Jest 的 TypeScript 类型定义,用于在使用 TypeScript 编写测试时提供类型检查和自动完成功能。

    3、babel-jest:这是用于将 Jest 集成到使用 Babel 的项目中的插件。它允许 Jest 处理通过 Babel 转换的代码。

    4、ts-jest:这是一个 Jest 转换器,用于处理 TypeScript 文件。它基本上允许 Jest 理解和运行 TypeScript 测试代码。

    5、jest-transform-stub:这个插件用于处理非 JavaScript 资源(如 CSS 和图片)的导入,这在 Jest 测试中通常会被忽略或需要特殊处理。

    npm install --save-dev jest @types/jest babel-jest ts-jest jest-transform-stub
    
    • 1

    6、@testing-library/jest-dom:提供一套针对 DOM 元素的 Jest 断言,非常适用于在测试 React 组件时使用。

    7、@testing-library/react:用于测试 React 组件,它提供了渲染组件、查询 DOM 元素以及与组件交互的工具。

    8、@testing-library/user-event:这个库用于模拟用户事件(如点击、输入等),可用于更逼真地测试用户交互。

    npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event
    
    • 1

    9、eslint-plugin-jest:这是一个 ESLint 插件,提供针对 Jest 测试的特定规则,有助于保持测试代码的质量和一致性。

    10、react-test-renderer:
    这是一个用于渲染 React 组件为 JavaScript 对象的库,常用于 Jest 的快照测试。它可以在不需要 DOM 环境的情况下测试 React 组件的输出,这对于在 Node 环境下运行的 Jest 测试非常有用。

    npm install --save-dev eslint-plugin-jest react-test-renderer
    
    • 1

    package.json

    1、基本的运行测试用例配置,npm test即可运行

    --watchAll:这个参数告诉 Jest 进入 “watch” 模式。在这个模式下, Jest 会监视项目中的文件变化。当修改并保存了代码文件(包括测试文件和被测试的源代码文件)时, Jest 会自动重新运行相关的测试。

    --watchAll--watch 不同之处在于,--watchAll 会在初次运行时执行所有测试,而 --watch 只在检测到文件更改时运行相关测试。

    "test": "jest --watchAll",
    
    • 1

    2、运行某个文件夹下的所有测试文件,src/tests代表文件夹路径

    "test:folder": "jest --watchAll --testPathPattern=src/tests",
    
    • 1

    3、单独运行某个测试文件,src/renderer/login/loginApi.test.tsx代表需要测试的文件路径

    "test:single": "jest --watchAll jest --findRelatedTests src/renderer/login/loginApi.test.tsx",
    
    • 1
    常见的 Jest 命令行操作

    1、f 只会跑测试未通过的用例,再次点击 f 会取消当前模式。

    2、o 只监听已改变的文件,如果存在多个测试文件,可以开启,会与当前 git 仓库中的提交进行比较,需要使用 git 来监听哪个文件修改了,也可以将 --watchAll 改为 --watch 只会运行修改的文件。

    3、a 运行所有测试,如果在 watch 模式中使用了 f 或 o ,使用 a 可以恢复运行所有测试。

    4、u 用于更新 Jest 快照测试中的快照。如果更改了渲染组件的输出,可以使用此命令更新快照。

    5、w 显示 Jest watch 模式中的所有可用命令和选项的列表。

    6、q 退出 Jest 的 watch 模式。

    7、i 只会运行之前运行失败的测试文件,但提供更交互式的体验。

    .babelrc

    当使用 Jest 测试一个使用 Babel 编译的项目时,Jest 会通过这些配置来正确处理和理解 JavaScript 代码。

    {
    	// 设置插件集合
    	"presets": [
    		// 使用当前插件,可以进行转换
    		// 数组的第二项为插件的配置项
    		[
    			"@babel/preset-env",
    			{
    				// 根据 node 的版本号来结合插件对代码进行转换
    				"targets": {
    					"node": "current"
    				}
    			}
    		]
    	]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    setupTests.js

    该文件设置测试环境中的全局对象和模拟(mock)某些模块,在本项目中针对 Electron 和 Node.js 的相关模块进行模拟,以便在不依赖实际 Electron 或浏览器环境的情况下测试特定的功能。

    /* eslint-disable no-undef */
    const electronMock = require('./__Mock__/electronMock')
    
    global.window.require = jest.fn(moduleName => {
    	if (moduleName === 'electron') {
    		return electronMock
    	}
    	if (moduleName === '@electron/remote') {
    		return {
    			require: jest.fn(module => {
    				// 模拟 Node.js 模块,如 fs
    				if (module === 'fs') {
    					return {} // 返回 fs 的模拟实现
    				}
    				// 其他模块模拟...
    			}),
    		}
    	}
    })
    global.window.matchMedia =
    	global.window.matchMedia ||
    	function () {
    		return {
    			matches: false,
    			addListener: function () {},
    			removeListener: function () {},
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    jest.config.ts

    jest.config.ts 是一个使用 TypeScript 编写的 Jest 配置文件。可以使用npx jest --init初始化命令来生成一个基本的配置文件。

    export default {
        // 自动清除 mock 调用和实例
        clearMocks: true,
        // 开启代码覆盖率收集
        collectCoverage: true,
        // 代码测试覆盖率通过分析那些文件生成的,!代表不要分析
        collectCoverageFrom: ['**/*.{ts,js,tsx}', '!**/node_modules/**', '!**/vendor/**'],
        // 代码覆盖率报告的输出目录
        coverageDirectory: 'coverage',
        // 代码覆盖率的收集器,这里使用 V8 引擎
        coverageProvider: 'v8',
        // 代码覆盖率报告的格式
        coverageReporters: [
            'text-summary',
            'lcov',
        ],
        globals: {
            'ts-jest': {
                // 关闭 ts-jest 的诊断信息
                diagnostics: false,
            },
        },
        // 引入模块时,进行自动查找模块类型,逐个匹配
        moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
        // 模块名字使用哪种工具进行映射
        moduleNameMapper: {
            '^@/(.*)$': '/src/$1', //将 @/ 映射到 src/ 目录
            '\\.(css|less)$': 'jest-transform-stub',
            '^localTypes$': '/src/types.ts',
            '^localUtils$': '/src/utils/index.ts',
            '^localConst$': '/src/utils/constants.ts',
            '^Assets/(.*)$': '/assets/$1',
        },
        preset: 'ts-jest',
        rootDir: undefined,
        // 检测从哪个目录开始,rootDir 代表根目录
        roots: ['/src'],
        // 在运行测试之前执行的文件(设置测试环境)
        setupFilesAfterEnv: ['./setupTests.js'],
        // 测试运行的环境,会模拟 dom
        testEnvironment: 'jsdom',
        // 哪些文件会被认为测试文件
        testMatch: [
            // src 下的所有 __tests__ 文件夹中的所有的 js jsx ts tsx 后缀的文件都会被认为是测试文件
            '/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
            // scr 下的所有以 .test/spec.js/jsx/ts/tsx 后缀的文件都会被认为是测试文件
            '/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
        ],
        // 测试时忽略的路径
        testPathIgnorePatterns: ['\\\\node_modules\\\\'],
        // 测试文件中引用一下后缀结尾的文件会使用对应的处理方式
        transform: {
            '^.+\\.(t|j)s$': 'ts-jest',
            '\\.svg$': '/__Mock__/svgTransform.js',
        },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    __Mock__文件夹

    文件夹用于存放模拟(mock)模块

    自动模拟:当调用jest.mock('moduleName')时,Jest会查找__mocks__文件夹中名为 moduleName 的文件,并自动使用该文件中的模拟实现。这意味着不需要在每个测试文件中手动设置模拟。

    第三方模块模拟:这个机制不仅适用于自定义模块,也适用于第三方模块。例如正在使用一个发送 fetch 请求的库,可以在 __mocks__文件夹中创建一个模拟,以避免在测试中发出真实的网络请求。

    Mock

    Mock fetch 或其他 HTTP 请求库的调用

    待补充

    Mock 函数

    jest.fn()
    
    • 1

    Mock 第三方模块

    待补充

    全局函数 describe 和 it

    describe 用于将测试分组,而 it 用于定义单个具体的测试用例。可以在 describe 块中放置多个 it 测试用例,也可以嵌套其他 describe 块以创建更详细的测试结构。

    // 用于创建一个测试套件,将一组功能或逻辑相关的测试用例组织在一起
    describe('测试输入框的校验规则', () => {
        // it 的第一个参数是一个字符串,描述了测试用例应该做什么,有助于代码的可读性和测试结果的理解
        it('输入正常', async () => {
            // ...
        });
        it('必填', async () => {
            // ...
        });
        it('仅支持汉字、字母、数字和-_%.', async () => {
            // ...
        })
        it('以数字、字母或汉字开头', async () => {
            // ...
        })
        it('限长', async () => {
            // ...
        })
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    断言 expect

    用于验证代码的行为是否符合预期。 expect 函数接受一个参数———想要测试的值。然后,expect 返回一个“期望对象”,这个对象提供了一系列“匹配器”(matcher)方法,用于声明对这个值的期望。

    describe('测试输入框的校验规则', () => {
        it('必填', async () => {
            // ...
            expect(message).toBeInTheDocument()
        })
        it('仅支持汉字、字母、数字和-_%.', async () => {
            // ...
            expect(message).toBeInTheDocument()
        })
        it('以数字、字母或汉字开头', async () => {
            // ...
            expect(message).toBeInTheDocument()
        })
        it('限长', async () => {
            // ...
            expect(message).toBeInTheDocument()
        })
        it('输入正常', async () => {
            // ...
            await waitFor(() => {
                expect(input.className).toMatch('ant-input-status-success')
            })
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    匹配器

    toBe :期待是否与匹配器中的值相等,相当于object.is ===

    toMatch :匹配当前字符串中是否含有这个值,支持正则

    toContain :用于检查数组或字符串是否包含特定项或子串

    toBeInTheDocument :判断某个元素是否在文档中,即是否已被渲染到 DOM 上

    toHaveProperty :用于检查对象是否具有特定属性,可以选择性地检查属性值

    toEqual :是“相等”,不是“相同”,相当于==

    toBeFalsy 和 toBeTruthy :检查一个值是否为假或真

    toBeNull :专门用来检查一个值是否为 null

    toBeDefined 和 toBeUndefined :这些断言用于检查变量是否已定义或未定义

    toThrow :用于检查函数是否抛出错误

    not:用于对断言取反

    snapshot 快照

    会在当前测试文件位于的文件夹下生成一个__snapshots__文件夹,该文件夹下会生成扩展名为 .snap 文件,文件会保存代码运行的结果(如渲染的组件树、数据结构等)。

    toMatchSnapshot 方法:接受一个参数是快照名称,字符串类型。

    expect(container).toMatchSnapshot('必填')
    
    • 1

    一定要是 container ,不能是 screen ,用 screen 不会保存 DOM 结构

    优势
    自动化比较:Jest 自动比较快照,减少了手动检查输出的需要。

    简化复杂结构的测试:对于复杂的对象或大型UI组件,编写传统测试断言可能很困难。快照测试可以轻松捕获整个结构。

    文档化变化:快照文件也可以作为代码行为的一种文档,让开发者和审阅者理解代码更改的影响。

    快照更新:当代码发生更改,导致快照不再匹配时,可以使用 jest --updateSnapshot 命令或jest -u命令来更新快照。

    测试用例覆盖率报告

    会在主文件夹下生成一个名为 coverage 的文件夹,打开里面的 html 就可以看到各个文件的覆盖率,通常包含以下几种主要的覆盖率类型:

    行覆盖率(Line Coverage):测量有多少行代码被测试用例执行过。如果一行代码在测试中至少被执行一次,那么这一行就被认为是覆盖了的。

    函数覆盖率(Function Coverage):测量有多少个函数或方法被测试用例调用过。即使函数内的某些行没有被执行,只要函数被调用,它就被认为是覆盖了的。

    分支覆盖率(Branch Coverage):测量代码中的每个if语句、循环、switch语句等的每个分支是否都被执行过。这是检查条件语句的完整性的重要指标。

    语句覆盖率(Statement Coverage):测量有多少个独立语句被测试执行过。这与行覆盖率类似,但关注的是语句的执行。

    React Testing Library

    render

    渲染 React 组件到一个虚拟的 DOM 环境中以便进行测试。

    render 函数接受一个 React 组件作为参数,并返回一个包含多个属性和方法的对象,例如 container 和 debug 。 container 可以调用各类查询函数在渲染的组件中查找元素, debug 可以打印出 baseElement 的内部HTML,用于调试。

    describe('测试输入框的校验规则', () => {
        it('输入正常', async () => {
            const Com = <Index />
            const container = render(Com)
            container.debug()
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    screen

    在使用 React Testing Library 进行测试时,通常会先用 render 函数渲染组件,然后用 screen 查询和操作元素。screen 对象可以在测试文件中全局访问,无需在每个测试中单独导入或创建。

    describe('测试输入框的校验规则', () => {
        it('输入正常', async () => {
            render(<Index />)
            screen.debug()
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    查询函数

    React Testing Library 提供了一系列的查询函数,用于在 Jest 测试中找到 DOM 节点。

    getBy…

    getByText: 根据文本内容查找元素。

    getByLabelText: 根据关联的 文本查找 ,