Jest前端自动化测试框架基础入门
一.前端要学的测试课
1.前端要学的测试
- Jest入门
- TDD实战
- BDD实战
2.前端工程化的一部分
- 前端自动化测试
- 高质量代码设计
- 高质量代码实现
3.前端自动化测试的例子
Vue、Echarts、React、Ant-Design…这些框架都有使用到自动化测试
4.前端为什么需要自动化测试
- 改需求时,代码重构:导致修复时间长,成本高
- 自动化测试,修复时间少,难度低
5.课程涵盖内容
二.前端要学的测试课
1.自动化测试背景及原理
作为一名前端开发开发者,每天都会遇到各种各样的 bug,比如安全性的 bug,逻辑 bug,性能 bug,展示 bug 等,在日常开发过程中想要不出 bug 几乎是不可能的。
当遇到复杂的业务场景或对代码进行修补的时候出现 bug 其实是非常正常的事情,bug 本身并不可怕,可怕的是把 bug 真正的带到线上。
所以为了防止 bug 上线,可以做些比如 codeview 的整合,通过测试同学的测试,帮助发现代码潜在的问题,或者通过灰度发布这样的机制帮助在代码上线之前进行局部的验证,这些方法都可以很好地帮助降低 bug 上线的概率。
但是对于前端来说还有没有更好的办法降低代码中 bug 出现的频率呢?是有的,一些简单错误可以通过以下几种工具来规避:
- TypeScript
- Flow
- EsLint
- StyleLint
- …
它们都可以帮助提高前端代码质量,减少 bug 数量,当然这还不够,还可以使用前端自动化测试工具来进一步避免 bug 的产生。
说到自动化测试,在后端自动化测试已经很普遍被应用了,但是在前端领域目前被普及的情况并不是很好,因此学习前端测试刻不容缓。
常见测试种类:
- 单元测试
- 集成测试
- end To end 端到端测试
- 回归测试
- 性能测试
- 压力测试
- …
为了更好的理解,接下来进入代码环节:
- 新建文件夹(mkdir lesson1)
- 打开新建的文件夹(cd lesson1)
- 创建文件(touch math.js)
function add(a,b) {
return a + b;
}
function minus(a,b) {
return a - b;
}
- 创建文件(touch index.html),引入 math.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>math.js</title>
<script src="math.js"></script>
</head>
<body>
</body>
</html>
- 创建文件(touch math.test.js)
let result = add(3, 7);
let expected = 10;
if(result !== expected) {
throw Error(`3 + 7 应该等于 ${expected},但是结果却是${result}`);
}
result = minus(3, 3);
expected = 0;
if(result !== expected) {
throw Error(`3 - 3 应该等于 ${expected},但是结果却是${result}`);
}
直接打开页面在浏览器的控制台,输入 math.test.js 中的测试代码并执行:
- index.html 已经引入了 math.js 这个函数库,可以通过直接在控制台执行的方式调用
- 测试过程中若是修改了原代码,需要刷新页面再执行测试代码
我们写的测试的例子基本上都是一个套路,就是 先预期一个结果,然后在执行计算出真正的结果,然后两个结果进行比较是否相同,如果相同就可以通过,如果不相同就抛出异常错误信息。
优化测试代码,单独封装一个 expect 函数,方便使用:
function expect(result) {
return {
toBe: function (actual) {
if (result !== actual) {
throw Error('预期值和实际值不相等');
}
}
}
}
expect(add(3,3)).toBe(6);
expect(minus(6,3)).toBe(3);
但是有个问题就是测试结果报错后从报错信息上看并不知道具体是函数库中的哪个方法错了。所以进一步优化:
function expect(result) {
return {
toBe: function (actual) {
if (result !== actual) {
throw Error(`预期值和实际值不相等 预期${actual} 结果却是${result}`);
}
}
}
}
function test (desc, fn) {
try {
fn();
console.log(`${desc} 通过测试`)
}catch (e) {
console.log(`${desc} 没有通过测试 ${e}`)
}
}
test('测试加法 3 + 7', () => {
expect(add(3,3)).toBe(6);
});
test('测试减法 6 - 3', () => {
expect(minus(6,3)).toBe(3);
});
2.前端自动化测试框架
在实际项目中只有 except 和 test 两个方法显然是不够的,同时还有很多自动化测试机制需要集成。
现在业界已经有很多前端自动化测试框架,这些框架里面集成了非常多的方法和机制供选用,在使用过程中的可以方便快捷的进行项目级别的前端自动化测试了。
目前业界主流的几个前端自动化测试框架包括
- Jasmine
- Mocha+Chai
- Jest
我们主要说说jest的优势:
- 速度快(在编辑过程中可以自动运行修改部分的测试代码)
- API 简单
- 易配置
- 隔离性好
- 监控模式
- IDE 整合
- 快照 Snapshot
- 多项目并行
- 覆盖率报告快速生成
- Mock 丰富
- 支持拓展性强,如:Babel、TypeScript、Node.js、React、Angular、Vue
3.使用Jest修改自动化测试样例
接下来开始使用 Jest,在这之前需要提前安装好 Node 环境,具体可参见https://blog.csdn.net/qq_32682301/article/details/106038232
- 新建文件夹(mkdir lesson2)
- 进入新建的目录(cd lesson2)
- 初始化 npm 包管理环境(npm init)
- 一路回车后可以看到目录下生成一个文件 —— package.json
- 安装 Jest (npm i jest@24.8.0 -D)
- -D 表示只有在开发的时候才会运行测试用例
- 安装好后将上节代码复制过来并作如下修改:
math.js 作为 node 模块导出:
function add(a,b) {
return a + b;
}
function minus(a,b) {
return a - b;
}
function multi(a,b) {
return a * b;
}
module.exports = {
add,
minus,
multi
}
math.test.js 中 math.js 作为 node 模块导入并使用
const math = require('./math')
const {
add,
minus,
multi
} = math
test('测试加法 3 + 7', () => {
expect(add(3,3)).toBe(6);
});
test('测试减法 6 - 3', () => {
expect(minus(6,3)).toBe(3);
});
test('测试乘法 3 * 3', () => {
expect(multi(3,3)).toBe(9);
});
修改配置 package.json:
{
...
"scripts": {
"test": "jest --watchAll"
},
...
}
–watchAll 表示监听所有测试用例,当有发生变化时,自动运行 jest 重跑所有测试用例
控制台运行 npm run test,结果如下:
> lesson2@1.0.0 test
> jest
PASS ./math.test.js
√ 测试加法 3 + 7 (4ms)
√ 测试减法 6 - 3
√ 测试乘法 3 * 3 (1ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 5.468s
Ran all test suites.
若是测试代码写错,结果如下:
> lesson2@1.0.0 test
> jest
FAIL ./math.test.js
√ 测试加法 3 + 7 (6ms)
√ 测试减法 6 - 3 (1ms)
× 测试乘法 3 * 3 (7ms)
● 测试乘法 3 * 3
expect(received).toBe(expected) // Object.is equality
Expected: 3
Received: 9
15 |
16 | test('测试乘法 3 * 3', () => {
> 17 | expect(multi(3,3)).toBe(3);
| ^
18 | });
at Object.<anonymous> (math.test.js:17:22)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 2 passed, 3 total
Snapshots: 0 total
Time: 4.258s
Ran all test suites.
为什么 math 中的方法必须导出来呢?
因为 Jest 在前端项目中帮助我们完成两类内容:
- 单元测试(模块测试)
- 集成测试(多个模块测试)
因此必须要以模块的方式来使用.不论是按照 CommonJS 还是 Es Module 改造,都需要符合 Jest 才能进行自动化测试
但是这样的话在 html 文件中使用会产生报错:
Uncaught ReferenceError: module is not defined
可以小改造一下(math.js):
...
try {
module.exports = {
add,
minus,
multi
}
} catch (e) {}
这种报错一般是不会出现在项目中的,现在的项目基本都是模块化编码
4.Jest的简单配置
jest 有默认配置,下面来自定义配置,在这之前需要生成配置文件,运行 npx jest --init (Jest\lesson2\jest.config.js 完整如下) :
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// Respect "browser" field in package.json when resolving modules
// browser: false,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\AImooc-Oliver\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: null,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: null,
// A path to a custom dependency extractor
// dependencyExtractor: null,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: null,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: null,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: null,
// Run tests from one or more projects
// projects: null,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: null,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: null,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: null,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: null,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: null,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
常见的配置里面都有,可以按需求选择注释或放开
运行 npx jest --coverage 可以查看覆盖率:
PASS ./math.test.js
√ 测试加法 3 + 7 (13ms)
√ 测试减法 6 - 3 (1ms)
√ 测试乘法 3 * 3 (1ms)
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
math.js | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 4.569s
Ran all test suites.
不仅在控制台有,在根目录也生成了相应文件:
.
|-- clover.xml
|-- coverage-final.json
|-- lcov-report
| |-- base.css
| |-- block-navigation.js
| |-- index.html
| |-- math.js.html
| |-- prettify.css
| |-- prettify.js
| |-- sort-arrow-sprite.png
| `-- sorter.js
`-- lcov.info
1 directory, 11 files
Jest/lesson2/coverage/lcov-report/index.html 可以直接访问:
可以修改配置 package.json:
{
...
"scripts": {
"coverage": "jest --coverage"
},
...
}
之后运行 npm run coverage,也是一样的效果
修改 Jest\lesson2\jest.config.js 中的 coverageDirectory 可以指定生成目录名称
一般前端项目中都是使用 ESModule 的语法,按 ESModule 改一下:
- math.js
function add(a,b) {
return a + b;
}
function minus(a,b) {
return a - b;
}
function multi(a,b) {
return a * b;
}
export {
add,
minus,
multi
}
- math.test.js
import {
add,
minus,
multi
} from './math'
test('测试加法 3 + 7', () => {
expect(add(3,3)).toBe(6);
});
test('测试减法 6 - 3', () => {
expect(minus(6,3)).toBe(3);
});
test('测试乘法 3 * 3', () => {
expect(multi(3,3)).toBe(9);
});
改完代码之后,运行 jest 会有报错产生,这是因为 jest 是运行在 node 环境,并不能直接识别 ESModule 的语法,这就要用到 babel 了。
安装 babel 相关依赖:
npm i @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
在根目录新建 babel 配置文件 .babelrc
{
"presets": [
[
"@babel/preset-env", {
"targets": {
"node": "current"
}
}
]
]
}
再次运行 jest ,成功!
jest 运行底层机制:
- npm run jest
- jest (babel-jest)
- babel-core
- .babelrc 配置
- 在运行测试之前,结合 babel,先把代码做一次转化
- 运行转化过的测试用例代码
5.Jest中的匹配器
什么是匹配器呢?在之前的案例中,toBe就是一个最基本的匹配器。
5.1 toBe匹配器
toBe 匹配器类似于 Object.is 或者 ===,精确相等。
test('测试toBe', () => {
expect(10).toBe(10); // passed
});
test('测试toBe', () => {
const a = {one: 1}
expect(a).toBe( {one: 1}); // failed,因为两个对象的地址是不一样的
});
5.2 toEqual匹配器
测试对象的内容是否相等,不比较对象的地址,只关心对象的内容是否一致,递归检查对象或数组的每个字段。
test('测试toEqual', () => {
const a = {one: 1}
expect(a).toEqual( {one: 1}); // passed
});
5.3 toBeNull匹配器
测试某个变量是否为null,如果是则Passed,否则failed。
test('测试toBeNull', () => {
const a = null
expect(a).toBeNull(); // passed
});
5.4 toBeUndefined匹配器和toBeDefined匹配器
测试某个变量是否未定义,如果是则Passed,否则failed
test('测试toBeUndefined', () => {
const a = undefined;
expect(a).toBeUndefined(); // passed
});
test('测试toBeUndefined', () => {
const a = '';
expect(a).toBeUndefined(); // failed
});
test('测试toBeUndefined', () => {
const a = null;
expect(a).toBeUndefined(); // failed
});
test('测试toBeDefined', () => {
const a = null;
expect(a).toBeDefined(); // passed
});
test('测试toBeDefined', () => {
const a = undefined;
expect(a).toBeDefined(); // failed
});
5.5 toBeTruthy匹配器
测试某个变量是否为真,如果是则Passed,否则failed
test('测试toBeTruthy', () => {
const a = undefined;
expect(a).toBeTruthy(); // undefined 视为false
});
test('测试toBeTruthy', () => {
const a = null;
expect(a).toBeTruthy(); // null视为false
});
test('测试toBeTruthy', () => {
const a = 0;
expect(a).toBeTruthy(); // 0 视为false
});
test('测试toBeTruthy', () => {
const a = 1;
expect(a).toBeTruthy(); // 1 视为true
});
5.6 toBeFalsy匹配器
测试某个变量是否为假,如果是则Passed,否则failed
test('测试toBeFalsy', () => {
const a = 1;
expect(a).toBeFalsy(); // failed,因为1 视为true
});
test('测试toBeFalsy', () => {
const a = undefined;
expect(a).toBeFalsy(); // passed,因为undefined 视为false
});
test('测试toBeFalsy', () => {
const a = null;
expect(a).toBeFalsy(); // passed,因为null 视为false
});
test('测试toBeFalsy', () => {
const a = 0;
expect(a).toBeFalsy(); // passed,因为0 视为false
});
test('测试toBeFalsy', () => {
const a = 0;
expect(a).not.toBeFalsy(); // failed,因为0 视为false,但是匹配器要的是真
});
5.7 数字相关的匹配器
test('测试toBeGreaterThan', () => {
const count = 10;
expect(count).toBeGreaterThan(9); // passed,表示希望count这个变量的值比9大
});
test('测试toBeLessThan', () => {
const count = 10;
expect(count).toBeLessThan(9); // failed,表示希望count这个变量的值比9小
});
test('测试toBeGreaterThanOrEqual', () => {
const count = 9;
expect(count).toBeGreaterThanOrEqual(9); // passed,表示希望count这个变量的值大于等于9
});
test('测试toBeLessThanOrEqual', () => {
const count = 9;
expect(count).toBeLessThanOrEqual(9); // passed,表示希望count这个变量的值小于等于9
});
test('测试toBeCloseTo', () => {
const firstNumber = 0.1;
const secondNumber = 0.2;
expect(firstNumber + secondNumber).toEqual(0.3); // 结果是failed,因为js计算浮点数的时
expect(value).toBe(0.3); // 这句会报错,因为浮点数有舍入误差候,有可能会溢出或者说不准确,这种情况下最好用toBeCloseTo
});
test('测试toBeCloseTo', () => {
const firstNumber = 0.3;
const secondNumber = 0.4;
expect(firstNumber + secondNumber).toBeCloseTo(0.7); // passed
});
5.8 字符串相关的匹配器
test('测试toMatch', () => {
const str = 'www.baidu.com';
expect(str).toMatch('baidu'); // passed, 表示str字符串中是否包含baidu这个字符串,是返回passed
expect(str).toMatch(/baidu/); //passed,这里还可以写正则表达式
});
5.9 数组相关的匹配器
test('测试toContain', () => {
const arr = ['dee', 'lee'];
expect(arr).toContain('dee'); // passed, 表示arr数组中是否包含dee这个字符串元素,是返回passed
});
test('测试toContain', () => {
const arr = ['dee', 'lee'];
const data = new Set(arr);
expect(data).toContain('dee'); // passed, 表示arr数组中是否包含dee这个字符串元素,是返回passed
});
5.10 异常情况的匹配器
const throwNewErrorFunc = () => {
throw new Error('this is a new error');
}
test('测试toThrow', () => {
expect(throwNewErrorFunc).toThrow(); // passed, 表示希望throwNewErrorFunc这个方法运行的时候能够抛出一个异常
});
test('测试toThrow', () => {
expect(throwNewErrorFunc).not.toThrow(); // failed, 表示希望throwNewErrorFunc这个方法运行的时候不能够抛出异常
});
test('测试toThrow', () => {
expect(throwNewErrorFunc).toThrow('this is a new error'); // passed, 表示希望throwNewErrorFunc这个方法运行的时候能够抛出一个异常,并且内容是'this is a new error'
expect(throwNewErrorFunc).toThrow(/this is a new error/); // 也可以是正则表达式
});
5.11 其它匹配器
参见jest官网https://www.jestjs.cn/docs/using-matchers。也要注意Expect断言。
6.Jest命令行工具的使用
Ctrl + Shift + P 打开 vscode 的命令窗口,输入
install code command
会显示如下:
直接点击,code 命令会添加到系统 path 中。
这样在随便一个命令行窗口输入 code 就可以打开 vscode,输入 code filePath/directoryPath 即可在 vscode 中打开对应文件或目录。
目前在 windows 中安装 vscode 过程中该命令会自动添加到 path 中
接下来这节内容便是和这命令行有关啦
运行 npm run test 之后,jest 会运行所有测试用例,最后会显示这样一句:
Watch Usage: Press w to show more.
输入 w 后,显示:
Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
模式 f
在修改测试用例所在文件后只运行上一次失败了的测试用例,其他跳过(skip)
PS:若是上一次运行成功,后面再改动也不会自动运行
模式 o
只运行与修改文件相关联的测试用例
需要借助 git 来获取文件变动记录,否则会报错
- 安装 git
- 项目根目录运行 git init 初始化一个 git 仓库
- 运行 git add . 将项目下的所有文件添加到 git 仓库
- 运行 git commit -m ‘version 1’ 将文件变化提交并做备注(到了这一步就已经满足 o 模式的运行条件啦)
- 运行 git push 将已提交的文件变动推送到线上 git 仓库
之前配置 package.json:“test”: “jest --watchAll” 默认会进入 a 模式,配置为 --watch 则会默认进入 o 模式:
{
...
"scripts": {
"test": "jest --watch"
},
...
}
模式 t
通过正则表达式过滤,只运行通过过滤的测试用例
模式 p
类似模式 t, 通过正则表达式过滤,只运行通过过滤的测试文件
q : 退出监听模式
7.异步代码的测试方法
安装 axios
npm i axios@0.19.0 -S
7.1 回调函数
新建 fetchData.js:
import axios from 'axios'
const fetchData = (fn) => {
axios.get('http://www.dell-lee.com/react/api/demo.json').then(res => fn(res.data))
}
export {fetchData }
新建单元测试文件 fetchData.test.js:
import {fetchData} from './fetchData'
// 回调类型异步函数的测试
test('fetchData 返回结果为 { success: true }', (done) => {
fetchData((data) => {
expect(data).toEqual({
success: true
})
// 只有当 done 函数被执行到才认为是测试用例执行结束
done();
})
})
不使用 done 的话,测试用例执行到 fetchData 之后直接就返回 pass
7.2 promise
还有一种情况,将 Promise 对象直接返回出来:修改 fetchData.js:
import axios from 'axios'
const fetchData = () => {
return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
export {fetchData }
相应修改单元测试文件 fetchData.test.js:
import {fetchData} from './fetchData'
test('fetchData 返回结果为 Promise: { success: true }', () => {
return fetchData().then((res) => {
expect(res.data).toEqual({
success: true
})
})
})
若是想要单独测试 404,可以修改为如下:
import {fetchData} from './fetchData'
test('fetchData 返回结果为 404', () => {
expect.assertions(1) // 下面的 expect 至少执行一个
return fetchData().catch((e) => {
expect(e.toString().indexOf('404') > -1).toBe(true)
})
})
若是不使用 expect.assertions ,当测试 接口访问成功,没走 catch 时,相当于啥也没有执行,也会通过,加上后若是接口访问成功会报错:Expected one assertion to be called but received zero assertion calls.
还有可以使用 expect 自带的函数识别结果:
test('fetchData 返回结果为 Promise: { success: true }', () => {
return expect(fetchData()).resolves.toMatchObject({
data: {
success: true
}
})
})
test('fetchData 返回结果为 404', () => {
return expect(fetchData()).rejects.toThrow()
})
7.3 async和await
除了使用 return 还可以使用 async…await 的语法:
test('fetchData 返回结果为 Promise: { success: true }', async () => {
await expect(fetchData()).resolves.toMatchObject({
data: {
success: true
}
})
})
test('fetchData 返回结果为 404', async () => {
await expect(fetchData()).rejects.toThrow()
})
还可以使用 async…await 先拿到响应结果,再判断:
test('fetchData 返回结果为 Promise: { success: true }', async () => {
const res = await fetchData()
expect(res.data).toEqual({
success: true
})
})
test('fetchData 返回结果为 404', async () => {
expect.assertions(1) // 下面的 expect 至少执行一个
try {
await fetchData()
} catch (e) {
expect(e.toString()).toEqual('Error: Request failed with status code 404.')
}
})
8.jest的钩子函数
Jest 中的钩子函数指的是在 Jest 执行过程中到某一特定时刻被自动调用的函数,类似 Vue/React 中的生命周期函数。
新建 Counter.js:
export default class Counter {
constructor() {
this.number = 0
}
addOne() {
this.number += 1
}
minusOne() {
this.number -= 1
}
}
新建 Counter.test.js:
import Counter from "./Counter";
describe('测试 Counter', () => {
const counter = new Counter();
test('测试 addOne 方法', () => {
counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 minusOne 方法', () => {
counter.minusOne()
expect(counter.number).toBe(0)
})
})
运行测试用例,直接通过,但是两个测试用例共用了一个实例 counter,相互之间有影响,这显然是不可以的,可以引入 Jest 的 钩子函数来做预处理。
修改 Counter.test.js
import Counter from "./Counter";
describe('测试 Counter', () => {
let counter = null
beforeAll(() => {
console.log('beforeAll')
})
beforeEach(() => {
console.log('beforeEach')
counter = new Counter();
})
afterEach(() => {
console.log('afterEach')
// counter = null
})
afterAll(() => {
console.log('afterAll')
})
test('测试 addOne 方法', () => {
console.log('测试 addOne ')
counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 minusOne 方法', () => {
console.log('测试 minusOne ')
counter.minusOne()
expect(counter.number).toBe(-1)
})
})
这样就不会相互之间产生影响了
编辑 Counter.js 新增两个方法
export default class Counter {
constructor() {
this.number = 0
}
addOne() {
this.number += 1
}
addTwo() {
this.number += 2
}
minusOne() {
this.number -= 1
}
minusTwo() {
this.number -= 2
}
}
这时候测试文件怎么写呢?很显然功能有分类,可以使用 describe:
编辑 Counter.test.js
import Counter from "./Counter";
describe('测试 Counter', () => {
let counter = null
beforeAll(() => {
console.log('beforeAll')
})
beforeEach(() => {
console.log('beforeEach')
counter = new Counter();
})
afterEach(() => {
console.log('afterEach')
// counter = null
})
afterAll(() => {
console.log('afterAll')
})
describe('测试“增加”相关的方法', () => {
test('测试 addOne 方法', () => {
console.log('测试 addOne ')
counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 addTwo 方法', () => {
console.log('测试 addTwo ')
counter.addTwo()
expect(counter.number).toBe(2)
})
})
describe('测试“减少”相关的方法', () => {
test('测试 minusOne 方法', () => {
console.log('测试 minusOne ')
counter.minusOne()
expect(counter.number).toBe(-1)
})
test('测试 minusTwo 方法', () => {
console.log('测试 minusTwo ')
counter.minusTwo()
expect(counter.number).toBe(-2)
})
})
})
测试日志如下:
测试 Counter
测试“增加”相关的方法
√ 测试 addOne 方法 (6ms)
√ 测试 addTwo 方法 (4ms)
测试“减少”相关的方法
√ 测试 minusOne 方法 (4ms)
√ 测试 minusTwo 方法 (4ms)
console.log Counter.test.js:8
beforeAll
console.log Counter.test.js:12
beforeEach
console.log Counter.test.js:27
测试 addOne
console.log Counter.test.js:17
afterEach
console.log Counter.test.js:12
beforeEach
console.log Counter.test.js:32
测试 addTwo
console.log Counter.test.js:17
afterEach
console.log Counter.test.js:12
beforeEach
console.log Counter.test.js:41
测试 minusOne
console.log Counter.test.js:17
afterEach
console.log Counter.test.js:12
beforeEach
console.log Counter.test.js:46
测试 minusTwo
console.log Counter.test.js:17
afterEach
console.log Counter.test.js:22
afterAll
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 4.411s
9.钩子函数的作用域
每一个 describe 都可以有自己的 beforeAll、afterAll、beforeEach、afterEach,执行顺序是从外往内。
外部的钩子函数可以对当前 describe 所有的测试用例起作用,而内部的只对内部的测试用例起作用,这就是钩子函数的作用域。
可以自行编写尝试,这里就不再赘述了。
还有一个单元测试小技巧,test 使用 only 修饰符可以让单元测试只运行这一个测试用例。
test.only('', () => {})
注意,代码执行顺序中,最先执行的是不包含在任何测试用例和钩子函数中的语句(直接暴露在各个 describe 内部最外层的语句),且只执行一次,后续才是测试用例和钩子函数的执行。
10.Jest 中的Mock
新建 lesson8.js:
export const runCallback = callback => {
callback();
}
要测试 runCallback 一般思路都是创建一个函数,返回一个值,只要 expect 最终拿到这个值就说明没问题,但是 runCallback 的主要功能是执行 callback,若是和本示例这样没有将 callback 返回,岂不是测不了了。。。
因此最佳实践来了,如下。
10.1 toBeCalled
新建 lesson8.test.js
import { runCallback } from "./lesson8";
test('测试 runCallback', () => {
const func = jest.fn();
runCallback(func);
expect(func).toBeCalled();
})
mock 一个函数,使用 toBeCalled 即可检测函数是否被调用。
10.2 func.mock
打印看一下 func.mock 里面有啥(console.log(func.mock)):
{
calls: [ [] ],
instances: [ undefined ],
invocationCallOrder: [ 1 ],
results: [ { type: 'return', value: undefined } ]
}
- calls:func 每次执行的入参列表的列表
- instances:每次执行 func 时创建的实例的列表(即 函数中this的指向,默认 undefined)
- invocationCallOrder:每次 func 执行的顺序列表(例如:1, 2, 3 表示按顺序执行)
- results:每次执行 func 的返回值列表
编辑 lesson8.test.js(执行两次 func)
import { runCallback } from "./lesson8";
test('测试 runCallback', () => {
const func = jest.fn();
runCallback(func);
runCallback(func);
expect(func).toBeCalled();
console.log(func.mock)
})
再看一下 func.mock 里面有啥:
{
calls: [ [], [] ],
instances: [ undefined, undefined ],
invocationCallOrder: [ 1, 2 ],
results: [
{ type: 'return', value: undefined },
{ type: 'return', value: undefined }
]
}
可以看到每一次调用在 func.mock 中都是留有痕迹的
编辑 lesson8.test.js(给 func 传入一个函数,并执行三次)
test('测试 runCallback', () => {
const func = jest.fn(() => 123); // mock 函数,捕获函数的调用
// func.mockImplementation(() => 123); // 功能同上一句
// func.mockImplementationOnce(() => 123); // 只模拟一次
// func.mockImplementationOnce(() => 456); // 只模拟一次
// func.mockImplementation(() => this);
// func.mockReturnThis(); // 作用同上一句
runCallback(func);
runCallback(func);
runCallback(func);
expect(func).toBeCalled();
// expect(func).toBeCalledWith(); // 断言每次调用的入参内容
console.log(func.mock)
})
打印结果:
{
calls: [ [], [], [] ],
instances: [ undefined, undefined, undefined ],
invocationCallOrder: [ 1, 2, 3 ],
results: [
{ type: 'return', value: 123 },
{ type: 'return', value: 123 },
{ type: 'return', value: 123 }
]
}
注意:带 Once 的要优先于不带的执行
10.3 mockReturnValue & mockReturnValueOnce
编辑 lesson8.test.js(让 func 通过 mockReturnValueOnce 来返回值)
test('测试 runCallback', () => {
const func = jest.fn(); // mock 函数,捕获函数的调用
func.mockReturnValueOnce('once')
runCallback(func);
runCallback(func);
runCallback(func);
expect(func).toBeCalled();
console.log(func.mock)
})
打印结果:
{
calls: [ [], [], [] ],
instances: [ undefined, undefined, undefined ],
invocationCallOrder: [ 1, 2, 3 ],
results: [
{ type: 'return', value: 'once' },
{ type: 'return', value: undefined },
{ type: 'return', value: undefined }
]
}
可以看到 ‘once’ 只返回了一次
编辑 lesson8.test.js(让 func 通过 mockReturnValueOnce, mockReturnValueOnce 链式调用,mockReturnValue 三种方式来返回值)
test('测试 runCallback', () => {
const func = jest.fn(); // mock 函数,捕获函数的调用
func.mockReturnValueOnce('1');
func.mockReturnValueOnce('2');
func.mockReturnValueOnce('3').mockReturnValueOnce('4').mockReturnValueOnce('5');
func.mockReturnValue('6'); // 后续每次返回同样的值
[...new Array(8)].map(() => runCallback(func))
expect(func).toBeCalled();
console.log(func.mock)
})
打印结果:
{
calls: [
[], [], [], [],
[], [], [], []
],
instances: [
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined, undefined
],
invocationCallOrder: [
1, 2, 3, 4,
5, 6, 7, 8
],
results: [
{ type: 'return', value: '1' },
{ type: 'return', value: '2' },
{ type: 'return', value: '3' },
{ type: 'return', value: '4' },
{ type: 'return', value: '5' },
{ type: 'return', value: '6' },
{ type: 'return', value: '6' },
{ type: 'return', value: '6' }
]
}
接下来通过示例理解一下 mock 里的 instances
编辑 lesson8.js(callback 运行一次,之后创建一个实例,并赋予一个属性作为标识)
export const runCallback = (callback, index) => {
callback();
let obj = new callback()
obj.name = 'callback_' + index
}
编辑 lesson8.test.js:
test('测试 runCallback', () => {
const func = jest.fn(); // mock 函数,捕获函数的调用
func.mockReturnValue('6'); // 后续每次返回同样的值
[...new Array(3)].map((item, index) => runCallback(func, index))
expect(func).toBeCalled();
console.log(func.mock)
})
打印结果:
{
calls: [ [], [], [], [], [], [] ],
instances: [
undefined,
mockConstructor { name: 'callback_0' },
undefined,
mockConstructor { name: 'callback_1' },
undefined,
mockConstructor { name: 'callback_2' }
],
invocationCallOrder: [ 1, 2, 3, 4, 5, 6 ],
results: [
{ type: 'return', value: '6' },
{ type: 'return', value: undefined },
{ type: 'return', value: '6' },
{ type: 'return', value: undefined },
{ type: 'return', value: '6' },
{ type: 'return', value: undefined }
]
}
可见每次运行默认不会有实例化对象,因此是 undefined,但是在实例化后,就会有一个命名为 mockConstructor 的构造对象
普通函数可以通过 jest.fn() 的方式来 mock,为保证当前部分功能的独立性,那接口请求也是需要 mock 的(避免网络和后端的影响)
编辑 lesson8.js(新增 getData 请求接口)
export const getData = () => {
return axios.get('/api').then(res => res.data)
}
编辑 lesson8.test.js
import { runCallback, getData } from "./lesson8";
import axios from 'axios'
jest.mock('axios');
...
test.only('测试 getData', async () => {
axios.get.mockResolvedValue({data: 'hello'})
await getData().then(data => {
expect(data).toBe('hello')
})
})
注意:jest.mock 必须放在文件的上面紧贴着 import,且外面不能嵌套任何内容否则对其mock的内容的作用域有影响(与被mock内容保持一致,且加载顺序挨着)
总结一下 mock 的几大功能:
- 捕获函数的调用和返回结果,以及 this 和调用顺序
- 可以自由设置返回结果
- 改变函数的内部实现