Jest单元测试框架
重要通知
Jest 支持 Babel、TypeScript、Node、React、Angular、Vue 等诸多框架!
基本概况
Jest 是一个来自于facebook出品的通用JavaScript 测试框架。
论单元测试的重要性
如何度量团队中每个人的代码质量?
如何保障团队中每个人的代码质量?
如何保证review代码的100%完整性与正确性?
敢随时轻易地重构旧有的业务代码吗?
在没有测试人员的情况下,敢随时发布上线业务代码吗?
函数、模块、类和组件
编写单元测试是为了验证小型、独立的代码单元是否按预期工作。单元测试通常涵盖单个函数、类、可组合项或模块。
__test__
index.test.ts
index.spec.ts
命名规范
只要满足规范一或规范二即可。
- 规范一: 与业务文件独立开来,新建文件夹__test__,单独存放测试用例
- 规范二: 与业务文件放在一起,例如业务文件为index.ts,则测试用例文件为index.test.ts或index.spec.ts
# 文件夹
__tests__
# 文件名称
.test.ts .test.js
.spec.ts .spec.js
安装配置
> pnpm install --save-dev jest
> jest --init
> pnpm run test
jest.config.js
module.exports = {
// 指定 rootDir 对应的路径,如果与 jest.config.js 文件所在的目录相同,则不用设置
rootDir: './',
// 使用 ts-jest 作为预处理器
// 参考:https://kulshekhar.github.io/ts-jest/docs/getting-started/presets
preset: 'ts-jest/presets/js-with-ts',
// 是否报告每个test的执行详情
verbose: true,
// 覆盖率结果输出的文件夹
coverageDirectory: '__tests__/coverage',
// 模块后缀名
moduleFileExtensions: ['js', 'json', 'ts', 'jsx', 'tsx'],
// 初始化配置文件路径
setupFiles: [
// 注意,如果不需要接入 enzyme 则不用引用此初始化文件
'<rootDir>/__tests__/setup/jest.setup.ts',
'<rootDir>/__tests__/setup/mock.setup.ts',
],
// 测试运行环境,jsdom类浏览器环境
testEnvironment: 'jsdom',
// 匹配测试文件
testMatch: [
'<rootDir>/editor-components/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
'<rootDir>/__tests__/*.{js,jsx,ts,tsx}',
],
// 忽略测试文件
testPathIgnorePatterns: ['/node_modules/'],
// 模块别名,注意要根据项目实际的 tsconfig.json 配置更新
moduleNameMapper: {
// 注意,如果不需要接入 enzyme 则不用设置对 css 文件的模拟导入
// 代理css文件的导入
'\\.(c|le)ss$': 'identity-obj-proxy',
// 简单模拟静态资源的导入,因为已经使用了下面的 transform 字段配置处理方式,所以此处注释掉
// '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
// '<rootDir>/__tests__/mock/file/index.js',
// 识别模块相对地址
'^(api|utils|actions|components|helpers|reducers)/(.*)$': '<rootDir>/src/$1/$2',
// 识别全局测试文件
'^__tests__/(.*)$': '<rootDir>/__tests__/$1',
},
// 因为要模拟实际的静态资源载入情况,所以指定了特定文件的处理器
transform: {
// 代理静态资源的导入
// 已经指定了预处理器为 ts-jest, 所以下面不用再手动指定 js 文件的处理器
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/__tests__/mock/file/transformer.js',
// 此种静态资源模拟导入方式,就需要显示的配置 babel-jest 为 js 文件的处理器
// 参考:https://jestjs.io/zh-Hans/docs/webpack#模拟-css-模块
// '\\.[jt]sx?$': ['babel-jest', { rootMode: 'upward' }],
"^.+\.vue$": "@vue/vue3-jest",
"^.+\js$": "babel-jest",
"^.+\\.ts$": "ts-jest"
},
// 编译时不忽略node_modules/@fe文件夹
transformIgnorePatterns: ['/node_modules/(?!@fe)'],
};
代码示例
- a.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
- a.test.js
import { describe, expect, test, it } from '@jest/globals';
const jestA = require('./a');
test('adds 1 + 2 to equal 3', () => {
expect(jestA(1, 2)).toBe(3);
});
属性与API接口
匹配器(Matcher)
import { describe, expect, test, it } from '@jest/globals';
test('描述语句', () => {
expect(...).toBe(...); // 等于 | Object.is()
expect(...).toEqual(...); // 对比 |
expect(...).toBeNull();
expect(...).toBeDefined();
expect(...).not.toBeUndefined();
expect(...).not.toBeTruthy();
expect(...).toBeFalsy();
// 非
expect(...).not.toBeNull();
// Numbers 数字
expect(...).toBeGreaterThan(...);
expect(...).toBeGreaterThanOrEqual(...);
expect(...).toBeLessThan(...);
expect(...).toBeLessThanOrEqual(...);
// Strings 字符串
expect(...).not.toMatch([RegExp]);
// Arrays and iterables 数组和可迭代对象
expect(...).toContain(...);
// Exceptions
expect(...).toThrow(...);
// 对于比较浮点数相等,使用 而不是 ,因为你不希望测试取决于一个小小的舍入误差。
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
});
修饰符
test('the best flavor is not coconut', () => {
expect(bestLaCroixFlavor()).not.toBe('coconut');
});
快照测试
当你想要确保你的UI不会有意外的改变,快照测试是非常有用的工具。典型的做法是在渲染了UI组件之后,保存一个快照文件,检测他是否与保存在单元测试旁的快照文件相匹配。 若两个快照不匹配,测试将失败:有可能做了意外的更改,或者UI组件已经更新到了新版本。
- 第一次运行此测试时,Jest 会创建一个快照文件。
- 在当前测用例文件同级目录创建一个__snapshots__目录,并在该目录中创建xxx.test.ts.snap的文件
import { describe, expect, test, it } from '@jest/globals';
import renderer from 'react-test-renderer';
import Link from '../Link';
it('renders correctly', () => {
const tree = renderer
.create(<Link page="http://www.facebook.com">Facebook</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
更新快照
--testNamePattern限制仅仅需要生成的一部分快照文件
> jest --updateSnapshot [--testNamePattern] # 全写
> jest -u [--testNamePattern] # 简写
> npm run test:unit -- -u
属性匹配器
项目中常常会有不定值字段生成(例如IDs和Dates)。如果你试图对这些对象进行快照测试,每个测试都会失败。针对这些情况,Jest允许为任何属性提供匹配器(非对称匹配器)。在快照写入或者测试前只检查这些匹配器是否通过,而不是具体的值。
it('will check the matchers and pass', () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: 'LeBron James',
};
expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
id: expect.any(Number),
});
});
测试异步代码
import { describe, expect, test, it } from '@jest/globals';
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
Vue3与vite集成Jest
# yarn
> yarn add jest jest-environment-jsdom babel-jest @babel/preset-env @vue/vue3-jest @vue/test-utils -D
# npm
> npm install --save-dev jest jest-environment-jsdom babel-jest @babel/preset-env @vue/vue3-jest @vue/test-utils
# 支持ts
> yarn add --dev ts-jest @types/jest
jest.config.js
module.exports = {
preset: 'ts-jest',
globals: {},
// 测试运行环境,jsdom类浏览器环境
testEnvironment: "jsdom",
transform: {
"^.+\.vue$": "@vue/vue3-jest",
// "^.+\\.vue$": "vue-jest",
"^.+\js$": "babel-jest",
"^.+\\.ts$": "ts-jest"
},
testRegex: "(/__tests__/.*|(\.|/)(test|spec))\.(js|ts)$",
moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
coveragePathIgnorePatterns: ["/node_modules/", "/tests/"],
coverageReporters: ["text", "json-summary"],
// Fix in order for vue-test-utils to work with Jest 29
// https://test-utils.vuejs.org/migration/#test-runners-upgrade-notes
testEnvironmentOptions: {
customExportConditions: ["node", "node-addons"],
},
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
testURL: 'http://localhost/',
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
coverageDirectory: 'coverage',
collectCoverageFrom: ['<rootDir>/src/**/*.{js,ts,vue}'],
coveragePathIgnorePatterns: ['^.+\\.d\\.ts$', 'src/runtimeEnv.ts'],
modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
cacheDirectory: '<rootDir>/tmp/cache/jest',
timers: 'fake',
}
babel.config.js
module.exports = {
env: {
test: {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
},
],
],
},
},
}
package.json
{
"scripts": {
"test:unit": "jest",
"": "jest --watch", # 默认执行 jest -o 监视有改动的测试
"": "jest --watchAll" # 监视所有测试
}
}