Jest

举报
SHQ5785 发表于 2024/06/27 09:16:38 2024/06/27
【摘要】 一、前言Jest 是由 Facebook 提供的开源 JavaScript 测试框架,特别适用于React和Node.js环境。它以简单的配置、高效的性能和易用性而闻名,旨在简化前端开发中的单元测试、集成测试、端到端测试和快照测试。Jest 提供了一套完整的测试解决方案,包括断言库、测试运行器、模拟工具等。此外,Jest还提供内置的代码覆盖率工具,帮助开发者优化测试范围,使得编写和运行测试...

一、前言

Jest 是由 Facebook 提供的开源 JavaScript 测试框架,特别适用于ReactNode.js环境。它以简单的配置、高效的性能和易用性而闻名,旨在简化前端开发中的单元测试集成测试端到端测试快照测试。Jest 提供了一套完整的测试解决方案,包括断言库测试运行器模拟工具等。此外,Jest还提供内置的代码覆盖率工具,帮助开发者优化测试范围,使得编写和运行测试变得更加简单和高效。

Jest 的一些主要特点和优势包括:

  • 开箱即用:Jest 内置了断言库、测试覆盖率报告等功能,无需额外配置即可开始编写测试。
  • 易于上手:Jest 提供了类似于 BDD(行为驱动开发)的语法,使用 describeit 等函数来组织测试,非常直观和易读。
  • 快照测试:Jest 支持对 React 组件进行快照测试,可以轻松捕获组件的结构变化,确保组件在修改后仍按预期渲染。
  • 模拟和间谍:Jest 内置了强大的模拟和间谍功能,可以模拟模块、函数行为,并监视函数调用情况,方便进行单元测试和集成测试。
  • 并行执行:Jest 支持并行执行测试用例,充分利用多核 CPU,加快测试速度。
  • 智能提示:Jest 与主流的 IDE 和编辑器(如 VSCode)集成,提供智能提示和自动补全,提高开发效率。

1.1 引入

要在项目中使用 Jest,首先需要通过 npmyarn 安装 Jest 依赖包。可以在项目根目录下运行以下命令:

npm install --save-dev jest

yarn add --dev jest

安装完成后,可以在 package.json 文件的 scripts 字段中添加 Jest 的测试命令,例如:

{
  "scripts": {
    "test": "jest"
  }
}

Jest 的配置文件通常命名为 jest.config.jsjest.config.ts,放置在项目根目录下。在配置文件中,可以自定义 Jest 的行为,如测试文件的匹配模式、测试环境的设置、测试覆盖率的阈值等。一个简单的 jest.config.js 文件示例如下所示:

module.exports = {
  testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
  testEnvironment: 'jsdom',
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: -10,
    },
  },
};

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: {
        '^@/(.*)$': '<rootDir>/src/$1', //将 @/ 映射到 src/ 目录
        '\\.(css|less)$': 'jest-transform-stub',
        '^localTypes$': '<rootDir>/src/types.ts',
        '^localUtils$': '<rootDir>/src/utils/index.ts',
        '^localConst$': '<rootDir>/src/utils/constants.ts',
        '^Assets/(.*)$': '<rootDir>/assets/$1',
    },
    preset: 'ts-jest',
    rootDir: undefined,
    // 检测从哪个目录开始,rootDir 代表根目录
    roots: ['<rootDir>/src'],
    // 在运行测试之前执行的文件(设置测试环境)
    setupFilesAfterEnv: ['./setupTests.js'],
    // 测试运行的环境,会模拟 dom
    testEnvironment: 'jsdom',
    // 哪些文件会被认为测试文件
    testMatch: [
        // src 下的所有 __tests__ 文件夹中的所有的 js jsx ts tsx 后缀的文件都会被认为是测试文件
        '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
        // scr 下的所有以 .test/spec.js/jsx/ts/tsx 后缀的文件都会被认为是测试文件
        '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
    ],
    // 测试时忽略的路径
    testPathIgnorePatterns: ['\\\\node_modules\\\\'],
    // 测试文件中引用一下后缀结尾的文件会使用对应的处理方式
    transform: {
        '^.+\\.(t|j)s$': 'ts-jest',
        '\\.svg$': '<rootDir>/__Mock__/svgTransform.js',
    },
}

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

{
	// 设置插件集合
	"presets": [
		// 使用当前插件,可以进行转换
		// 数组的第二项为插件的配置项
		[
			"@babel/preset-env",
			{
				// 根据 node 的版本号来结合插件对代码进行转换
				"targets": {
					"node": "current"
				}
			}
		]
	]
}

1.2 基础语法

1.2.1 全局函数 describe 和 it

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

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

1.2.2 断言 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 匹配器

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

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

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

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

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

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

  • toBeFalsytoBeTruthy :检查一个值是否为假或真;

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

  • toBeDefinedtoBeUndefined :这些断言用于检查变量是否已定义或未定义;

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

  • not:用于对断言取反;

1.2.4 snapshot 快照

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

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

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

注意⚠️:一定要是 container ,不能是 screen ,用 screen 不会保存 DOM 结构!

优势

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

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

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

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

1.2.5 测试用例覆盖率报告

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

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

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

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

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

1.2.6 React Testing Library render

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

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

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

1.2.7 screen

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

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

1.2.8 查询函数

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

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

  • getByLabelText: 根据关联的 <label> 文本查找 <input>, <select>, 或 <textarea> 元素。

  • getByPlaceholderText: 根据占位符文本查找输入框。

  • getByAltText: 根据图片的 alt 属性文本查找图片元素。

  • getByTitle: 根据 title 属性查找元素。

  • getByRole: 根据 ARIA 角色查找元素。

  • getByTestId: 根据 data-testid 属性查找元素。

queryBy…
queryBy…函数的行为类似于 getBy… 函数,但当查询的元素不存在时,它们返回 null 而不是抛出错误。这对于断言某个元素不在页面上非常有用。

findBy…
findBy…函数是 getBy… 函数的异步版本。它们返回一个 Promise,适用于等待异步操作完成后元素出现在 DOM 中的情况。

…AllBy…, queryAllBy…, findAllBy…
这些函数的行为类似于 getBy…, queryBy…, 和 findBy…,但用于返回多个匹配的元素。如果没有找到匹配的元素,getAllBy… 和 findAllBy… 会抛出错误,而 queryAllBy… 返回一个空数组。

总结:
getBy… 函数用于当确定元素存在时。如果元素不存在,测试将失败。
queryBy… 函数用于当元素可能不存在,需要处理这种情况时。
findBy… 函数用于处理异步逻辑,当需要等待元素出现时。
…AllBy… 函数用于处理有多个匹配元素的情况。

// findByText参数必须是完整的文本,如果是子字符串,需要加上{exact: false}
// findByText不管前缀是screen还是container都可以成功
describe('测试输入框的校验规则', () => {
	it('仅支持汉字、字母、数字和-_%.', async () => {
		const Com = <Index />
		const container = render(Com)
		const input = await screen.findByRole('textbox')
		await userEvent.type(input, '@')
		const messages = await container.findByText('溶剂名称仅支持汉字、字母、数字和-_%.')
	})
})
describe('测试输入框的校验规则', () => {
	it('仅支持汉字、字母、数字和-_%.', async () => {
		const Com = <Index />
		const container = render(Com)
		const input = await screen.findByRole('textbox')
		await userEvent.type(input, '@')
		const messages = await screen.findByText('仅支持汉字、字母、数字和-_%.', {exact: false})
	})
})

1.2.9 waitFor

用于处理异步操作和元素的异步更新。waitFor 常与异步查询函数(如 findBy…)结合使用,用于处理组件状态更新或数据加载。

describe('测试输入框的校验规则', () => {
    it('输入正常', async () => {
        const container = render(<Index />)
        screen.debug()
        const input = await screen.findByRole('textbox')
        await waitFor(() => {
            expect(screen.getByText('必填', { exact: false })).toBeInTheDocument()
        })
    })
})

1.2.10 fireEvent 和 userEvent

Jest 提供fireEventuserEvent模拟用户操作。

  • fireEvent:直接同步触发 DOM 事件。当调用 fireEvent 的任何方法时(如 fireEvent.click),它会立即生成对应的 DOM 事件,并同步地传递给目标元素。因此,fireEvent 方法调用后不会返回 Promise,也不涉及任何异步操作,所以通常不需要使用 await 关键字。

  • userEvent:旨在更贴近用户的实际操作,因此它经常涉及到一系列复杂的、可能是异步的事件。例如,当用户在输入框中输入文字时,这不仅仅是一个简单的同步操作。它包含了一系列的键盘和输入事件,这些事件可能会触发各种事件处理器,这些处理器本身可能是异步的。

1、fireEvent来自’@testing-library/react’,userEvent来自@testing-library/user-event

2、fireEvent的清空 Input 输入框操作为fireEvent.change(input, {target: {value: ‘’}}),userEvent的清空 Input 输入框操作为userEvent.type(input, ‘{backspace}’)

3、fireEvent前不需要添加await,userEvent需要。

总结:如果需要模拟简单的事件并需要完全控制这些事件的属性,fireEvent 是个好选择。而如果需要模拟更复杂或更接近真实用户行为的交互,userEvent 则更合适。

describe('测试输入框的校验规则', () => {
	it('仅支持汉字、字母、数字和-_%.', async () => {
		const Com = <Index />
		const container = render(Com)
		const input = await screen.findByRole('textbox')
		fireEvent.change(input, {target: {value: '@'}})
	})
})
describe('测试输入框的校验规则', () => {
	it('仅支持汉字、字母、数字和-_%.', async () => {
		const Com = <Index />
		const container = render(Com)
		const input = await screen.findByRole('textbox')
		await userEvent.type(input, '@')
	})
})

二、Jest 基本用法和示例

2.1 工具支持

Jest同时提供丰富的工具支持,涉及的安装包如下:

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

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

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

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

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

npm install --save-dev jest @types/jest babel-jest ts-jest jest-transform-stub
  • @testing-library/jest-dom:提供一套针对 DOM 元素的 Jest 断言,非常适用于在测试 React 组件时使用。

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

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

npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event
  • eslint-plugin-jest:这是一个 ESLint 插件,提供针对 Jest 测试的特定规则,有助于保证测试代码的质量和一致性。

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

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

2.2 配置拓展

前面小结讲到,Jest 安装完成后,在 package.json 文件的 scripts 字段中添加 Jest 测试命令之后,便可进行简单的Jest测试,例如:

{
  "scripts": {
    "test": "jest"
  }
}
 npm run test

除了基础配置,还可以指定特殊配置以满足不同需求。

package.json 配置文件如下:

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

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

"test": "jest --watchAll",

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

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

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

"test:single": "jest --watchAll jest --findRelatedTests src/renderer/login/loginApi.test.tsx",

在 Jest 中,测试代码通常组织为测试套件Test Suite)和测试用例Test Case)。测试套件用于对相关测试用例进行分组,通常使用 describe 函数来定义。测试用例则是具体的测试场景,使用 ittest 函数来定义。每个测试用例中,可以使用断言(Assertion)来验证被测代码的行为是否符合预期。Jest 提供了丰富的断言函数和匹配器(Matcher),如 expecttoBetoEqual 等,用于进行值的比较和判断。

除了断言外,Jest 还提供了模拟Mock)和间谍Spy)功能,用于隔离被测代码的依赖,控制函数的行为和监视函数的调用情况。通过模拟和间谍,可以创建仿真对象和函数,模拟异步操作、文件系统等,使得测试更加可控和独立。

在 Jest 中编写测试用例非常简单,使用 test()it() 函数即可定义一个测试用例。这两个函数的用法相同,都接受两个参数:一个描述测试用例的字符串一个包含测试代码的回调函数。在测试用例中,使用断言函数(如 expect())和匹配器(如 toBe()toEqual())来验证被测代码的行为是否符合预期。

以下是一个简单的测试用例示例:

test('adds 1 + 2 to equal 3', () => {
  expect(1 + 2).toBe(3);
});

当被测代码包含异步操作时,Jest 提供了几种方式来测试异步代码。一种方式是使用回调函数和 done() 参数。在测试用例的回调函数中,调用 done() 表示异步操作完成。Jest 会等待 done() 被调用后才结束测试用例。

另一种测试异步代码的方式是使用 Promise。可以在测试用例中返回一个 Promise,并使用 resolvesrejects 匹配器来验证 Promise 的状态和结果。

如果使用 ES2017 的 async/await 语法,可以像编写同步代码一样编写异步测试用例,Jest 会自动处理异步操作。

下面是一个异步测试用例的示例:

test('fetches data from API', async () => {
  const data = await fetchData();
  expect(data).toEqual({ id: 1, name: 'John' });
});

Jest 还提供了测试 React 组件的功能。可以使用 render() 函数将组件渲染为虚拟 DOM,并使用 screen 对象查询渲染后的元素。通过 fireEvent 工具,可以模拟用户的交互事件,如点击、输入等。

此外,Jest 支持对 React 组件进行快照测试。快照测试可以捕获组件的结构,并将其与之前保存的快照进行比较,以确保组件在修改后仍然按预期渲染。

以下是一个 React 组件的测试示例:

import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';

test('renders and updates correctly', () => {
  render(<MyComponent />);
  expect(screen.getByText('Hello, World!')).toBeInTheDocument();

  fireEvent.click(screen.getByText('Click Me'));
  expect(screen.getByText('Button Clicked')).toBeInTheDocument();

  expect(screen.getByTestId('my-component')).toMatchSnapshot();
});

对于使用 Redux 进行状态管理的应用,Jest 可以用于测试 Action Creator 和 Reducer。通过创建模拟的 Redux 存储,可以测试 Action 是否正确派发以及 Reducer 是否正确更新状态。可以使用 redux-mock-store 库来模拟 Redux 存储,并检查 Action 的派发情况。

以下是一个 Redux Reducer 的测试示例:

import reducer from './reducer';
import * as actions from './actions';

test('should handle ADD_TODO', () => {
  const initialState = [];
  const newTodo = { id: 1, text: 'New Todo' };

  const newState = reducer(initialState, actions.addTodo(newTodo));

  expect(newState).toEqual([newTodo]);
});

通过这些示例,可以看到 Jest 提供了丰富的功能和工具,使得编写各种类型的测试变得简单和直观。无论是同步代码、异步代码、React 组件还是 Redux 状态管理,Jest 都能够很好地满足测试需求,提高代码的质量和可维护性。

2.3 Jest 测试案例

测试 Input 输入框的校验规则
当前 Input 输入框的校验规则:

(1)必填
(2)限长100
(3)仅支持汉字、字母、数字和-_%.
(4)必须以数字、字母或汉字开头

const nameRules = ({
    label,
    max = 10,
    required = true,
}: {
    label: string
    max?: number
    required?: boolean
}): Rule[] => [
        { required, message: `${label}必填` },
        { type: 'string', max, message: `${label}限长${max}` },
        {
            pattern: /^([a-zA-Z0-9\u4E00-\u9FA5_.%-])*$/g,
            message: `${label}仅支持汉字、字母、数字和-_%.`,
        },
        {
            pattern: /^([0-9|a-zA-Z0-9|\u4E00-\u9FA5])/g,
            message: `${label}以数字、字母或汉字开头`,
        },
    ]

因被测试组件的复杂程度不同,测试同一个功能所用的 API 也不同。

(1)被测试功能组件的简单版:
该组件只有基本的页面布局和nameRules校验规则

/* eslint-disable react-hooks/rules-of-hooks */
import { nameRules } from '@/utils/constants'
import { Form, Input } from 'antd'

const myInput = () => {
    return (
        <Form>
            <Form.Item
                label="Username"
                name="username"
                // 校验规则
                rules={nameRules({
                    label: '名称',
                    required: true,
                })}
            >
                <Input />
            </Form.Item>
        </Form>
    )
}
export default myInput

在测试较简单的组件时,模拟用户操作可以使用fireEvent.change(),断言也无需包裹在waitFor中便可同步执行。

/* eslint-disable no-undef */
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import Index from './index'
import '@testing-library/jest-dom'

describe('测试输入框的校验规则', () => {
    it('必填', async () => {
        // 渲染组件
        const Com = <Index />
        const container = render(Com)
        // findByRole不管前缀是screen还是container都可以成功
        const input = await screen.findByRole('textbox')
        // 在 input 输入框中输入1
        fireEvent.change(input, { target: { value: '1' } })
        // 清空 input
        fireEvent.change(input, { target: { value: '' } })
        // findByText参数必须是完整的文本,如果是子字符串,需要加上{exact: false}
        expect(await container.findByText('必填', { exact: false })).toBeInTheDocument()
    })
    it('仅支持汉字、字母、数字和-_%.', async () => {
        const Com = <Index />
        const container = render(Com)
        const input = await screen.findByRole('textbox')
        fireEvent.change(input, { target: { value: '@' } })
        expect(
            await container.findByText('仅支持汉字、字母、数字和-_%.', { exact: false })
        ).toBeInTheDocument()
    })
    it('以数字、字母或汉字开头', async () => {
        const Com = <Index />
        const container = render(Com)
        const input = await screen.findByRole('textbox')
        fireEvent.change(input, { target: { value: '-' } })
        expect(
            await container.findByText('以数字、字母或汉字开头', { exact: false })
        ).toBeInTheDocument()
    })
    it('限长', async () => {
        const Com = <Index />
        const container = render(Com)
        const input = await screen.findByRole('textbox')
        fireEvent.change(input, { target: { value: 'a'.repeat(101) } })
        expect(await container.findByText('限长', { exact: false })).toBeInTheDocument()
    })
    it('输入正常', async () => {
        const Com = <Index />
        const container = render(Com)
        const input = await screen.findByRole('textbox')
        fireEvent.change(input, { target: { value: '1' } })
        await waitFor(() => {
            expect(input.className).toMatch('ant-input-status-success')
        })
    })
})

(2)被测试功能组件的复杂版:
该组件是个集合组件,功能比较复杂,被测试的输入框只是其中一小部分内容。

因为组件存在 fetch 接口的请求,但是 jest 测试不会运行真实的 fetch 接口,所以需要 mock 数据,在本组件中通过在catch中给定初始数据。

在复杂环境下render组件时,需要 mock 渲染组件所需的各项参数,在本组件中id值是直接给定一个存在的 id ,onCancel方法 mock 一个空函数,Dn初始化数据。

此时模拟用户操作须使用await userEvent.type(),断言外须包裹await waitFor(() => {})

/* eslint-disable no-undef */
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import ComplexIndex from './ComplexIndex'
import '@testing-library/jest-dom'
import userEvent from '@testing-library/user-event'

describe('测试输入框的校验规则', () => {
    const onCancelMock = jest.fn()

    it('必填', async () => {
        // 渲染组件
        render(
            <ComplexIndex
                id="93e"
                onCancel={onCancelMock}
                Dn={{
                    dn1: 1,
                    dn2: '',
                }}
            />
        )
        const input = await screen.findByRole('textbox')
        // 在 input 输入框中输入“正常输入”
        await userEvent.type(input, '1')
        // 清空 input
        await userEvent.type(input, '{backspace}')
        // 异步等待断言执行
        await waitFor(() => {
            expect(screen.getByText('必填', { exact: false })).toBeInTheDocument()
        })
    })
    it('正常输入', async () => {
        render(
            <ComplexIndex
                id="93e"
                onCancel={onCancelMock}
                Dn={{
                    dn1: 1,
                    dn2: '',
                }}
            />
        )
        const input = await screen.findByRole('textbox')
        await userEvent.type(input, '正常输入')
        await waitFor(() => {
            expect(input.className).toMatch('ant-input-status-success')
        })
    })
    it('限长', async () => {
        render(
            <ComplexIndex
                id="93e"
                onCancel={onCancelMock}
                Dn={{
                    dn1: 1,
                    dn2: '',
                }}
            />
        )
        const input = await screen.findByRole('textbox')
        await userEvent.type(input, 'a'.repeat(101))
        await waitFor(() => {
            expect(screen.getByText('限长', { exact: false })).toBeInTheDocument()
        })
    })
    it('仅支持汉字、字母、数字和-_%.', async () => {
        render(
            <ComplexIndex
                id="93e"
                onCancel={onCancelMock}
                Dn={{
                    dn1: 1,
                    dn2: '',
                }}
            />
        )
        const input = await screen.findByRole('textbox')
        await userEvent.type(input, '@')
        await waitFor(() => {
            expect(
                screen.getByText('仅支持汉字、字母、数字和-_%.', { exact: false })
            ).toBeInTheDocument()
        })
    })
    it('以数字、字母或汉字开头', async () => {
        render(
            <ComplexIndex
                id="93e"
                onCancel={onCancelMock}
                Dn={{
                    dn1: 1,
                    dn2: '',
                }}
            />
        )
        const input = await screen.findByRole('textbox')
        await userEvent.type(input, '-')
        await waitFor(() => {
            expect(screen.getByText('以数字、字母或汉字开头', { exact: false })).toBeInTheDocument()
        })
    })
})

(3)获取原始 DOM 内容进行测试
Input 标签有aria-describedby属性,该属性的属性值是某个div的id,该div下的div包含所有类型的报错字样。

/* eslint-disable no-undef */
import { fireEvent, render, screen } from '@testing-library/react'
import Index from './index'
import '@testing-library/jest-dom'
import userEvent from '@testing-library/user-event'

describe('测试输入框的校验规则', () => {
    it('仅支持汉字、字母、数字和-_%.', async () => {
        // 渲染被测组件
        const Com = <Index />
        const container = render(Com)
        // 获取input元素
        const input = await screen.findByRole('textbox')
        // 在input输入框中输入@
        await userEvent.type(input, '@')
        // 获取input元素
        const inputEl = document.querySelector("input[type='text']")
        // 获取input元素的所有属性
        const attributes = inputEl!.attributes
        let ariaDescribedby = ''
        for (let i = 0; i < attributes?.length; i++) {
            console.log(attributes[i].name, attributes[i].value)
            // 找到aria-describedby属性
            if (attributes[i].name === 'aria-describedby') {
                // 获取 aria-describedby 属性的值
                ariaDescribedby = attributes[i].value
            }
        }
        // div 的 id 值为 aria-describedby 属性的值
        const borderDiv = document.getElementById(ariaDescribedby)
        const childrenDiv = borderDiv?.querySelectorAll('div')
        childrenDiv?.forEach(div => {
            // 报错文本
            console.log(div.textContent)
        })
    })
})

三、Jest 高级特性和最佳实践

Jest 提供了一些高级特性和最佳实践,可以帮助我们编写更加高效、可维护的测试代码。

首先,Jest 提供了钩子函数,用于在测试用例执行的不同阶段执行设置和清理操作。常用的钩子函数包括 beforeEach()afterEach()beforeAll()afterAll()beforeEach()afterEach() 会在每个测试用例执行前后被调用,而 beforeAll()afterAll() 则在所有测试用例执行前后被调用一次。通过使用钩子函数,我们可以在测试之间共享设置和清理代码,避免重复编写相同的逻辑。

Jest 还提供了强大的模块模拟功能,允许模拟外部模块的行为,以便隔离被测代码的依赖。使用 jest.mock() 函数可以模拟整个模块,并指定模拟的实现。使用 jest.fn() 可以创建一个模拟函数,用于跟踪函数的调用情况和返回值。使用 jest.spyOn() 可以监视真实模块中的函数调用,并在测试后恢复原有的实现。通过模块模拟,可以控制外部依赖的行为,使测试更加可控和稳定。

代码覆盖率是衡量测试质量的重要指标,Jest 内置了生成代码覆盖率报告的功能。通过在 Jest 配置文件中启用覆盖率收集,并运行测试命令,Jest 会自动生成详细的代码覆盖率报告,包括语句覆盖率分支覆盖率函数覆盖率行覆盖率等。可以通过配置覆盖率阈值,确保测试覆盖率达到一定的标准。此外,对于一些不需要测试的文件和代码块,可以使用注释或配置文件进行排除,以提高覆盖率的准确性。

在编写 Jest 测试时,遵循一些最佳实践和技巧可以提高测试的可读性和可维护性。以下是一些建议:

  • 合理组织和命名测试文件,通常将测试文件与被测代码文件放在同一目录下,并以 .test.js.spec.js 作为文件扩展名。
  • 使用描述性的测试用例名称,清晰表达测试的目的和预期行为,如 'should return the correct result when given valid input'
  • 保持测试用例的独立性和可重复性,每个测试用例应该能够独立运行,不依赖于其他测试用例的执行顺序或状态。
  • 使用工厂函数和辅助函数来简化测试代码,抽象出通用的设置和断言逻辑,提高测试可读性和可维护性。

示例代码:

// 使用钩子函数共享设置和清理代码
beforeEach(() => {
  // 在每个测试用例执行前进行设置
  jest.resetModules();
  jest.clearAllMocks();
});

afterEach(() => {
  // 在每个测试用例执行后进行清理
  cleanup();
});

// 使用模块模拟
jest.mock('./api');
import { fetchData } from './api';

test('should fetch data successfully', async () => {
  fetchData.mockResolvedValue({ id: 1, name: 'John' });

  const result = await someFunction();
  expect(result).toEqual({ id: 1, name: 'John' });
  expect(fetchData).toHaveBeenCalledTimes(1);
});

// 使用工厂函数简化测试代码
function createTestUser(overrides) {
  return {
    id: 1,
    name: 'John',
    email: 'john@example.com',
    ...overrides,
  };
}

test('should update user profile', () => {
  const user = createTestUser({ name: 'John Doe' });
  const updatedUser = updateProfile(user, { email: 'johndoe@example.com' });
  expect(updatedUser).toEqual({
    id: 1,
    name: 'John Doe',
    email: 'johndoe@example.com',
  });
});

通过应用这些高级特性和最佳实践,可以编写更加健壮、可维护的 Jest 测试,提高代码质量和开发效率。

四、项目实战 Vue项目集成Jest进行单元测试

4.1 浅渲染

在测试用例中,我们通常希望专注在一个孤立的单元中测试组件,避免对其子组件的行为进行间接的断言。

额外的,对于包含许多子组件的组件来说,整个渲染树可能会非常大。重复渲染所有的子组件可能会让我们的测试变慢。

Vue Test Utils 允许通过 shallowMount 方法只挂载一个组件而不渲染其子组件 (即保留它们的存根):

import { shallowMount } from '@vue/test-utils'

const wrapper = shallowMount(Component)
wrapper.vm // 挂载的 Vue 实例

4.2 应用全局插件和混入

有些组件可能依赖一个全局插件或混入 (mixin) 的功能注入,比如 vuexvue-router

如果为一个特定的应用撰写组件,可以在测试入口一次性设置相同的全局插件和混入。但是有些情况下,比如测试一个可能会跨越不同应用共享的普通组件套件时,最好在隔离设置中测试组件,不对全局的 Vue 构造函数注入任何东西。可使用 createLocalVue 方法来存档它们:

import { createLocalVue, mount } from '@vue/test-utils'

// 创建一个扩展的 `Vue` 构造函数
const localVue = createLocalVue()

// 正常安装插件
localVue.use(MyPlugin)

// 在挂载选项中传入 `localVue`
mount(Component, {
  localVue
})

注意⚠️:有些插件会为全局 Vue 构造函数添加只读属性,比如 Vue Router。这使得无法在一个 localVue 构造函数上二次安装该插件,或伪造这些只读属性。

4.3 仿造注入

另一个注入 prop 的策略就是简单的仿造它们。可以使用 mocks 选项:

import { mount } from '@vue/test-utils'

const $route = {
  path: '/',
  hash: '',
  params: { id: '123' },
  query: { q: 'hello' }
}

mount(Component, {
  mocks: {
    // 在挂载组件之前添加仿造的 `$route` 对象到 Vue 实例中
    $route
  }
})

4.4 处理路由

因为路由需要在应用的全局结构中进行定义,且引入了很多组件,所以最好集成到 end-to-end 测试。对于依赖 vue-router 功能的独立的组件来说,可使用上面提到的仿造注入技术仿造它们。

4.5 项目实战

安装 JestVue Test Utils

npm install --save-dev jest @vue/test-utils

接下来在 package.json 里定义一个 test:unit 脚本。

// package.json
{
  // ..
  "scripts": {
    // ..
    "test:unit": "jest"
  }
  // ..
}

在 Jest 中执行单文件组件
为了讲解 Jest 如何处理 *.vue 文件,我们需要安装并配置 vue-jest 预处理器:

npm install --save-dev vue-jest

然后在 package.json 里创建一个 jest 块:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      // 告诉 Jest 处理 `*.vue` 文件
      "vue"
    ],
    "transform": {
      // 用 `vue-jest` 处理 `*.vue` 文件
      ".*\\.(vue)$": "vue-jest"
    },
    "testURL": "http://localhost/"
  }
}

为 Jest 配置 TypeScript
为了在测试中使用 TypeScript 文件,我们需要在 Jest 中设置编译 TypeScript。为此我们需要安装 ts-jest:

npm install --save-dev ts-jest

接下来,需要在 package.json 中的 jest.transform 中加入一个入口告诉 Jest 使用 ts-jest 处理 TypeScript 测试文件:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // 用 `ts-jest` 处理 `*.ts` 文件
      "^.+\\.tsx?$": "ts-jest"
    }
    // ...
  }
}

放置测试文件
默认情况下,Jest 将会在整个工程里递归地找到所有的 .spec.js.test.js 扩展名文件。

需要改变 package.json 文件里的 testRegex 配置项以运行 .ts 扩展名的测试文件。

在 package.json 中添加以下 jest 字段:

{
  // ...
  "jest": {
    // ...
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
  }
}

Jest 推荐在被测试的代码旁边创建一个 __tests__ 目录,但也可以根据自己的风格组织测试文件。只是要注意 Jest 会在进行截图测试的时候在测试文件旁边创建一个 __snapshots__ 目录。

撰写一个单元测试

创建一个 src/components/__tests__/HelloWorld.spec.ts 文件,并加入如下代码:

// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

describe('HelloWorld.vue', () => {
  test('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

以上就是 TypeScriptVue Test Utils 一起工作所需要的全部工作

五、拓展阅读

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。