Vue进阶(八十八)前端测试工具介绍
文章目录
- 一、前言
- 1.1 引入
- 1.2 基础语法
- 1.2.1 全局函数 describe 和 it
- 1.2.2 断言 expect
- 1.2.3 匹配器
- 1.2.4 snapshot 快照
- 1.2.5 测试用例覆盖率报告
- 1.2.6 React Testing Library render
- 1.2.7 screen
- 1.2.8 查询函数
- 1.2.9 waitFor
- 1.2.10 fireEvent 和 userEvent
- 二、Jest 基本用法和示例
- 2.1 工具支持
- 2.2 配置拓展
- 2.3 Jest 测试案例
- 三、Jest 高级特性和最佳实践
- 四、项目实战 Vue项目集成Jest进行单元测试
- 4.1 浅渲染
- 4.2 应用全局插件和混入
- 4.3 仿造注入
- 4.4 处理路由
- 4.5 项目实战
- 五、拓展阅读
一、前言
Jest 是由 Facebook 提供的开源 JavaScript 测试框架,特别适用于React和Node.js环境。它以简单的配置、高效的性能和易用性而闻名,旨在简化前端开发中的单元测试、集成测试、端到端测试和快照测试。Jest 提供了一套完整的测试解决方案,包括断言库、测试运行器、模拟工具等。此外,Jest还提供内置的代码覆盖率工具,帮助开发者优化测试范围,使得编写和运行测试变得更加简单和高效。
Jest 的一些主要特点和优势包括:
- 开箱即用:Jest 内置了断言库、测试覆盖率报告等功能,无需额外配置即可开始编写测试。
- 易于上手:Jest 提供了类似于 BDD(行为驱动开发)的语法,使用
describe、it等函数来组织测试,非常直观和易读。 - 快照测试:Jest 支持对 React 组件进行快照测试,可以轻松捕获组件的结构变化,确保组件在修改后仍按预期渲染。
- 模拟和间谍:Jest 内置了强大的模拟和间谍功能,可以模拟模块、函数行为,并监视函数调用情况,方便进行单元测试和集成测试。
- 并行执行:Jest 支持并行执行测试用例,充分利用多核 CPU,加快测试速度。
- 智能提示:Jest 与主流的 IDE 和编辑器(如 VSCode)集成,提供智能提示和自动补全,提高开发效率。
1.1 引入
要在项目中使用 Jest,首先需要通过 npm 或 yarn 安装 Jest 依赖包。可以在项目根目录下运行以下命令:
npm install --save-dev jest
或
yarn add --dev jest
安装完成后,可以在 package.json 文件的 scripts 字段中添加 Jest 的测试命令,例如:
{"scripts": {"test": "jest"}
}
Jest 的配置文件通常命名为 jest.config.js 或 jest.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'],// 测试运行的环境,会模拟 domtestEnvironment: '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:是“相等”,不是“相同”,相当于==; -
toBeFalsy和toBeTruthy:检查一个值是否为假或真; -
toBeNull:专门用来检查一个值是否为null; -
toBeDefined和toBeUndefined:这些断言用于检查变量是否已定义或未定义; -
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 组件作为参数,并返回一个包含多个属性和方法的对象,例如 container 和 debug 。 container 可以调用各类查询函数在渲染的组件中查找元素, 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: 根据关联的 -
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 提供fireEvent和userEvent模拟用户操作。
-
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:这是Jest的TypeScript类型定义,用于在使用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 函数来定义。测试用例则是具体的测试场景,使用 it 或 test 函数来定义。每个测试用例中,可以使用断言(Assertion)来验证被测代码的行为是否符合预期。Jest 提供了丰富的断言函数和匹配器(Matcher),如 expect、toBe、toEqual 等,用于进行值的比较和判断。
除了断言外,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,并使用 resolves 或 rejects 匹配器来验证 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: stringmax?: numberrequired?: 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.Itemlabel="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 输入框中输入1fireEvent.change(input, { target: { value: '1' } })// 清空 inputfireEvent.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(<ComplexIndexid="93e"onCancel={onCancelMock}Dn={{dn1: 1,dn2: '',}}/>)const input = await screen.findByRole('textbox')// 在 input 输入框中输入“正常输入”await userEvent.type(input, '1')// 清空 inputawait userEvent.type(input, '{backspace}')// 异步等待断言执行await waitFor(() => {expect(screen.getByText('必填', { exact: false })).toBeInTheDocument()})})it('正常输入', async () => {render(<ComplexIndexid="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(<ComplexIndexid="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(<ComplexIndexid="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(<ComplexIndexid="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!.attributeslet 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) 的功能注入,比如 vuex 和 vue-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 项目实战
安装 Jest 和 Vue 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)})
})
以上就是 TypeScript 和 Vue Test Utils 一起工作所需要的全部工作
五、拓展阅读
- Jest官网
- Vue Test Utils
- Vue 官方测试指导
相关文章:
Vue进阶(八十八)前端测试工具介绍
文章目录 一、前言1.1 引入1.2 基础语法1.2.1 全局函数 describe 和 it1.2.2 断言 expect1.2.3 匹配器1.2.4 snapshot 快照1.2.5 测试用例覆盖率报告1.2.6 React Testing Library render1.2.7 screen1.2.8 查询函数1.2.9 waitFor1.2.10 fireEvent 和 userEvent 二、Jest 基本用…...
【录制,纯正人声】OBS录制软件,音频电流音,杂音解决办法,录制有噪声的解决办法
速度解决的方法 (1)用RNNoise去除噪声。RNNoise是一个开源的,效果不好的噪声去除器。使用方法就是点击滤镜,然后加噪声抑制RNNoise。【这方法不好用】 (2)用Krisp(https://krisp.ai/) 去除噪声。这个Kris…...
Django中drf动态过滤查询
Django中drf动态过滤查询 1、page.py 代码: from rest_framework.pagination import PageNumberPaginationclass UserPagination(PageNumberPagination):"""用户分页器"""page_size = 10 # 默认的页面数据数量page_query_param = page # 定…...
GTSAM | gtsam::PriorFactor
文章目录 概述一、定义介绍二、功能作用三、主要内容四、实例演示概述 本节介绍了GTSAM中的gtsam::PriorFactor类。 一、定义介绍 gtsam::PriorFactor 是 GTSAM(Graph-based Trajectory and Mapping)库中的一个类,用于定义先验因子。在因子图优化中,先验因子用于将一些变量…...
MMSegmentation改进:增加Kappa系数评价指数
将mmseg\evaluation\metrics\iou_metric.py文件中的内容替换成以下内容即可: 支持输出单类Kappa系数和平均Kappa系数。 使用方法:将dataset的config文件中:val_evaluator 添加mKappa,如 val_evaluator dict(typemmseg.IoUMetri…...
专栏【汇总】
专栏【汇总】 前言版权推荐专栏【汇总】付费 汇总置顶在读在学我的面试计算机重要课程java面试Java基础数据存储Java框架java提高计算机科学与技术课程算法杂项 最后 前言 2024-5-12 21:13:02 以下内容源自《【专栏】》 仅供学习交流使用 版权 禁止其他平台发布时删除以下此…...
成功解决IndexError: index 0 is out of bounds for axis 1 with size 0
成功解决IndexError: index 0 is out of bounds for axis 1 with size 0 🛠️ 成功解决IndexError: index 0 is out of bounds for axis 1 with size 0摘要引言正文内容(详细介绍)🤔 错误分析:为什么会发生IndexError&…...
C# MES通信从入门到精通(11)——C#如何使用Json字符串
前言 我们在开发上位机软件的过程中,经常需要和Mes系统进行数据交互,并且最常用的数据格式是Json,本文就是详细介绍Json格式的类型,以及我们在与mes系统进行交互时如何组织Json数据。 1、在C#中如何调用Json 在C#中调用Json相关…...
ON DUPLICATE KEY UPDATE 子句
ON DUPLICATE KEY UPDATE 是 MySQL 中的一个 SQL 语句中的子句,主要用于在执行 INSERT 操作时处理可能出现的重复键值冲突。当尝试插入的记录导致唯一索引或主键约束冲突时(即试图插入的记录的键值已经存在于表中),此子句会触发一…...
perl use HTTP::Server::Simple 轻量级 http server
cpan -i HTTP::Server::Simple 返回:已是 up to date. 但是我在 D:\Strawberry\perl\site\lib\ 找不到 HTTP\Server 手工安装:下载 HTTP-Server-Simple-0.52.tar.gz 解压 tar zxvf HTTP-Server-Simple-0.52.tar.gz cd D:\perl\HTTP-Server-Simple-…...
【STM32】基于I2C协议的OLED显示(利用U82G库)
【STM32】基于I2C协议的OLED显示(利用U82G库) 文章目录 【STM32】基于I2C协议的OLED显示(利用U82G库)一、实验背景二、U8g2介绍(一)获取(二)简介 三、实践(一)CubexMX配置(二)U8g2配…...
掌握Python3输入输出:轻松实现用户交互、日志记录与数据处理
Python 是一门简洁且强大的编程语言,广泛应用于各个领域。在 Python 编程中,输入和输出是基本而重要的操作。无论是进行用户交互、记录日志信息,还是将计算结果输出到控制台或文件,掌握这些操作都是编写高效 Python 程序的关键。本…...
用于每个平台的最佳WordPress LMS主题
你已选择在 WordPress 上构建学习管理系统 (LMS)了。恭喜! 你甚至可能已经选择了要使用的 LMS 插件,这已经是成功的一半了。 现在是时候弄清楚哪个 WordPress LMS 主题要与你的插件配对。 我将解释 LMS 主题和插件之间的区别,以便你了解要…...
pytorch 加权CE_loss实现(语义分割中的类不平衡使用)
加权CE_loss和BCE_loss稍有不同 1.标签为long类型,BCE标签为float类型 2.当reduction为mean时计算每个像素点的损失的平均,BCE除以像素数得到平均值,CE除以像素对应的权重之和得到平均值。 参数配置torch.nn.CrossEntropyLoss(weightNone,…...
【iOS】UI——关于UIAlertController类(警告对话框)
目录 前言关于UIAlertController具体操作及代码实现总结 前言 在UI的警告对话框的学习中,我们发现UIAlertView在iOS 9中已经被废弃,我们找到UIAlertController来代替UIAlertView实现弹出框的功能,从而有了这篇关于UIAlertController的学习笔记…...
django支持https
测试环境,可以用django自带的证书 安装模块 sudo pip3 install django_sslserver服务端https启动 python3 manage.py runsslserver 127.0.0.1:8001https访问 https://127.0.0.1:8001/quota/api/XXX...
算法题day41(补5.27日卡:动态规划01)
一、动态规划基础知识:在动态规划中每一个状态一定是由上一个状态推导出来的。 动态规划五部曲: 1.确定dp数组 以及下标的含义 2.确定递推公式 3.dp数组如何初始化 4.确定遍历顺序 5.举例推导dp数组 debug方式:打印 二、刷题…...
【附带源码】机械臂MoveIt2极简教程(四)、第一个入门demo
系列文章目录 【附带源码】机械臂MoveIt2极简教程(一)、moveit2安装 【附带源码】机械臂MoveIt2极简教程(二)、move_group交互 【附带源码】机械臂MoveIt2极简教程(三)、URDF/SRDF介绍 【附带源码】机械臂MoveIt2极简教程(四)、第一个入门demo 目录 系列文章目录1. 创…...
基于蚁群算法的二维路径规划算法(matlab)
微♥关注“电击小子程高兴的MATLAB小屋”获得资料 一、理论基础 1、路径规划算法 路径规划算法是指在有障碍物的工作环境中寻找一条从起点到终点、无碰撞地绕过所有障碍物的运动路径。路径规划算法较多,大体上可分为全局路径规划算法和局部路径规划算法两大类。其…...
政务云参考技术架构
行业优势 总体架构 政务云平台技术框架图,由机房环境、基础设施层、支撑软件层及业务应用层组成,在运维、安全和运营体系的保障下,为政务云使用单位提供统一服务支撑。 功能架构 标准双区隔离 参照国家电子政务规范,打造符合标准的…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...
docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...
【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...
oracle与MySQL数据库之间数据同步的技术要点
Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异ÿ…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
