Jest是一种JavaScript测试框架,用于编写单元测试以确保代码的正确性和可靠性。单元测试是指对代码中的最小单元进行测试,例如函数或方法,以验证其是否按预期工作。同时,编写测试用例可以促使开发人员编写更好的代码,包括更好的代码设计和更好的代码可读性。
1、jest覆盖率指标
- line coverage:行覆盖率;
- function coverage:函数覆盖率(不仅仅包含文件中自定义的函数,还包含外部引入的函数);
- branch:分支覆盖率(if else;|| 、??、三元运算符等都属是);
- Statement:语句覆盖率(代码语句,简单的是分号结尾的赋值、return语句,复合语句是if、try、for、switch等)。
2、jest插桩
Jest 一般使用 Istanbul 作为其代码覆盖率工具。覆盖率指标原理是函数插桩(instrument),例如:
greeting.js
function greeting(name = "HZFE") {
if (DEBUG) {
name = `${name} (DEBUG)`;
}
const foo = DEBUG ? "(DEBUG) foo" : "foo";
for (const i = 0; i < name.length; i++) {
name += "!";
}
return `Hello ${name}`;
}
经过 nyc instrument 后,大致代码如下:
out.js
function cov_g4lly5ec() {
// ...
var coverageData = {
path: "...",
statementMap: {
0: { start: { line: 2, column: 2 }, end: { line: 4, column: 3 } },
// ...
},
fnMap: {
0: {
name: "greeting",
decl: { start: { line: 1, column: 9 }, end: { line: 1, column: 17 } },
loc: { start: { line: 1, column: 33 }, end: { line: 13, column: 1 } },
line: 1,
},
},
branchMap: {
0: {
loc: { start: { line: 1, column: 18 }, end: { line: 1, column: 31 } },
type: "default-arg",
locations: [
{ start: { line: 1, column: 25 }, end: { line: 1, column: 31 } },
],
line: 1,
},
// ...
},
s: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 },
f: { 0: 0 },
b: { 0: [0], 1: [0, 0], 2: [0, 0] },
// ...
};
// ...
return actualCoverage;
}
cov_g4lly5ec();
function greeting(name = (cov_g4lly5ec().b[0][0]++, "HZFE")) {
cov_g4lly5ec().f[0]++;
cov_g4lly5ec().s[0]++;
if (DEBUG) {
cov_g4lly5ec().b[1][0]++;
cov_g4lly5ec().s[1]++;
name = `${name} (DEBUG)`;
} else {
cov_g4lly5ec().b[1][1]++;
}
const foo =
(cov_g4lly5ec().s[2]++,
DEBUG
? (cov_g4lly5ec().b[2][0]++, "(DEBUG) foo")
: (cov_g4lly5ec().b[2][1]++, "foo"));
cov_g4lly5ec().s[3]++;
for (const i = (cov_g4lly5ec().s[4]++, 0); i < name.length; i++) {
cov_g4lly5ec().s[5]++;
name += "!";
}
cov_g4lly5ec().s[6]++;
return `Hello ${name}`;
}
f、s、b分别表示function、statement和branch。在cov_g4lly5ec().b[1][0]++;中 1 代表第一个分支语句,1 后面紧跟的 0 代表分支的第一个叉路。
3、jest编程注意事项
- 外部导入
(1)不能直接import方法并模拟,实际使用的方法并不是通过import导入并mock的。如下:
import * as SaveApply from '../xx/SaveModel';
import { save } from '../xx/SaveModel';
// ok
SaveApply.save = jest.fn().mockResolvedValue({ resultCode: 200 });
// wrong
save = jest.fn().mockResolvedValue({ resultCode: 200 });
(2)可以使用Jest.mock来模拟整个模块或者模块的一部分。jest.mock是放到文件开头全局使用。
import { save } from '../xx/SaveModel';
jest.mock('../xx/SaveModel', () => {
const origin = jest.requireActual('../xx/SaveModel');
return {
...origin,
save: jest.fn()
};
});
- jest.fn和jest.spyOn区别
jest.fn和jest.spyOn都可以用来mock一个函数,区别是jest.fn mock的函数不会去执行,而jest.spyOn mock的函数是会去正常执行的,并提供mockRestore来恢复。
首先,真正执行代码有没有意义呢? jest.spyOn() 监视一个对象的方法时,我们可以确保 mock 函数与原始方法的行为是一致的,从而可以测试代码的实际行为。如果我们只是手动创建一个 mock 函数,并假定它的行为与原始方法相同,我们可能会遇到一些意想不到的问题,因为我们无法确保 mock 函数的行为与原始方法的行为完全一致。例如:
// 一个判断折扣的model
const Discount = {
checkDiscount: name => (
if(name === 'apple') {
return true;
}
return false;
};
const calculatePrice = (goods, checkDiscount) => {
let total = 0;
goods.forEach(item => {
let price = Number(item.price) * Number(item.count);
if(checkDiscount(item.name)) {
price *= 0.5;
}
total += price;
});
return total;
};
下面对calculatePrice函数进行test,如果直接mock则永远发现不了折扣是否发生改变。
describe('test', () => {
const goods = [
{name: 'apple', price: 20, count: 2},
{name: 'banana', price: 10, count: 1}
];
const spyCheckDiscount = jest.spyOn(Discount, 'checkDiscount');
// mock假数据
// spyCheckDiscount.mockReturnValueOnce(false).mockReturnValueOnce(true);
test('calculatePrice', () => {
expect(calculatePrice(goods, spyCheckDiscount).toBe(30));
});
});
其次,jest.spyOn的监视作用,如下:
const obj = {
method: (a, b) => a + b,
};
jest.spyOn(obj, 'method');
console.log(obj.method(1, 2)); // 输出 3
console.log(obj.method.mock.calls); // 输出 [ [ 1, 2 ] ]
console.log(obj.method.mock.calls.length); // 输出 1
- 其他
Jest.fn()默认返回的是underfined。toBeNull/toBeUndefined/toBeTruthy/toBeFalsy等,均是toEqual的简单封装,用于检测返回值的,而toBeCalled等是检测函数是否调用的。