随着前端实现的业务逻辑愈发复杂,前端单元测试的重要性也水涨船高。
而好的单元测试,不仅能够保证代码质量,还能够提高开发效率。
本文就来谈谈,如何写好前端单元测试。
背景
简单介绍下背景,笔者在工作中负责开发前端 SDK,测试框架使用 jest。
我们项目对代码测试覆盖率有一定的要求,需要满足:
{
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
比如我们需要测试这个函数:
export const getConfig = (name) => {
return {
foo: () => {},
name: name || "Anonymous",
};
};
对应的测试用例一般是这样的:
describe("getConfig", () => {
test("should return name if have the paramter", () => {
const name = "zxf4399";
const config = getConfig(name);
expect(config.name).toBe(name);
});
test("should return default name if doesn't have the paramter", () => {
const config = getConfig();
expect(config.name).toBe("Anonymous");
});
});
执行结果如下:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 50 | 100 |
index.js | 100 | 100 | 50 | 100 |
----------|---------|----------|---------|---------|-------------------
Jest: "global" coverage threshold for functions (80%) not met: 50%
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.444 s, estimated 1 s
Ran all test suites.
ELIFECYCLE Test failed. See above for more details.
由于 functions 的覆盖率没有达到 80%,所以测试失败了。
解决问题-Phase1
之前的解决方案:
test("should test foo function", () => {
const config = getConfig();
config.foo && config.foo();
});
结果:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.448 s, estimated 1 s
Ran all test suites.
我们发现 functions 的覆盖率居然达到了 100%,但是我并没有在 should test foo function
测试用例中调用 expect 函数,这合理吗?
答案是合理的。
虽然我们没有在测试用例中调用 expect 函数,但是我们在测试用例中调用了 config.foo 函数,而 config.foo 函数是在 getConfig 函数中定义的,所以 jest 会认为我们测试了这个函数。
但实际上这个测试行为是没有任何意义的,我们只是为了达到测试覆盖率的要求而这样写。
解决问题-Phase2
解决 Phase1 问题我们需要知道 jest 的测试覆盖率是如何计算的。
jest 的 coverage provider 有两种,分别是:
- istanbuljs
- c8
由于我们项目中使用的是 coverageProvider
是 babel, 所以我们只要关注 istanbuljs 就可以了。
针对这种 noop funciton 的测试用例,我们可以使用 istanbul ignore next
来忽略这个函数的测试覆盖率。
export const getConfig = (name) => {
return {
foo: /* istanbul ignore next */ () => {},
name: name || "Anonymous",
};
};
当然更加推荐的做法是抽象出一个 noop function,然后在测试用例中调用这个函数。
/* istanbul ignore next */
export const noop = () => {};
export const getConfig = (name) => {
return {
foo: noop,
name: name || "Anonymous",
};
};
最终的结果也是令人满意的:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.352 s, estimated 1 s
Ran all test suites.
本小节 Demo 传送门
如何写好单元测试
Phase1 与 Phase2 都是解决问题的方法,从技术上讲 Phase2 更合理一些(本身 noop function 就不应该是一个考核的点)但你也不能说 Phase1 的解决方案就是错误的。
所以我们需要思考如何写好单元测试