单元测试是前端知识体系中重要的一环,是中高级前端必须掌握的基础知识。然而在重视开发效率的日常工作中,单元测试往往没有被应用起来。
这是我写的《别说你不会单元测试》系列文章的第二篇,在学习单元测试的过程中,你可能会认识很多新名词,以及海量测试相关的 NPM 库(比如 Mocha/Jest/Sinon/Instanbul/Jamine...)。这些名词是什么意思?这些库的职责又是什么?接下来的内容将一一介绍。
往期文章链接在这里:
- 《别说你不会单元测试(一)认识单元测试》
断言
从最基本的内容说起,无论是那种测试类型,都必须有正确性校验(如果分辨不出成功与否,那就没意义了鸭)。在代码中,我们使用断言来判断被测试代码的执行结果和预期是否一致。
维基百科上是这样描述的:
写一个最基础的例子来理解断言究竟是怎么一回事:
创建一个 assert.js
文件,复制以下代码,代码中我们写了一个 sum
方法,它将两个参数相加并返回。用 node
运行它 node assert.js
看看是会发生什么。
const assert = require("assert");
const sum = (a, b) => {
return a + b;
};
assert(sum(2, 3) === 5, "2 plus 3 should equals to 5");
不出意外的话(比如你没有装 node...),程序正常运行结束,什么都不会发生 ? 。因为这是符合我们预期的,2 + 3 的结果就是等于 5。弄个反例试一下呢,比如修改一下代码,把 5 改成 6 再执行一遍。
assert(sum(2, 3) === 6, "2 plus 3 should equals to 6");
??? 恭喜解锁第一个报错,不必惊慌,开发阶段报错和线上奔溃比起来,根本不叫事儿~OK, 通过这个简单的例子,相信你肯定 get
到断言的作用了,就是在程序不符合预期的时候抛出异常,发出告警。
assert.js:385
throw err;
^
AssertionError [ERR_ASSERTION]: 2 plus 3 should equals to 6
上面的例子中,我们用到了 NodeJS 的 assert 模块。如果不想额外引入模块,自己写一个最基础的断言方法也很容易:
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
除了作为方法直接调用,assert
模块还提供了一些其他的方法,提供严格相等,正则匹配,异常断言等等的方法,可以直接看文档不一一列举了。
assert.deepStrictEqual(actual, expected[, message])
assert.match(string, regexp[, message])
assert.throws(fn[, error][, message])
...
社区还有很多其他的断言工具库,它们提供更灵活的断言风格,更丰富的工具方法,更优雅的异常展示等等。
- 7.2k stars 的 chai
- TJ 大神写的 should.js
- ...
测试替身(Test Double)
回顾上一篇文章,我们提到单元测试会比端对端测试更加稳定,这就要求被测试代码不能有过多依赖(依赖越多,稳定性越差)。
而实际情况是...被测试代码往往会和后端接口,定时器、动画、事件、第三方模块等等捆绑在一块,导致单元测试速度变慢,结果不稳定等等问题。
例如下面的例子,单元测试阶段我们不需要和后端接口一块测试,需要测的是这个方法本身的逻辑。
const getUserStars = async (userId) => {
const { level } = await fetch(`/api/user/${userId}`, {
method: "GET",
});
return "⭐⭐⭐⭐⭐".slice(0, level);
};
想要保证测试的速度和稳定性,就会用到测试替身。那怎么用呢,留个悬念。由于这部分的内容比较重要,可以展开讲一讲,临时决定将其作为下一篇文章的内容~
社区比较受欢迎的测试替身库:
- Sinonjs.
- testdouble
覆盖率
写了测试代码,就会需要一个维度来衡量测试的程度怎么样,覆盖率就是用来描述测试覆盖程度的,通常有四种方式来统计覆盖率。
- 语句覆盖(Statements)
- 分支覆盖 (Branches)
- 函数覆盖 (Functions)
- 行覆盖 (Lines)
通过下面的例子来看看有哪些区别,创建一个 coverage.js
,复制以下代码。
const add = (a, b) => {
return a + b;
};
const multi = (a, b) => {
return a * b;
};
let flag1 = true;
let flag2 = false;
if (flag1 || flag2) {
add(2, 3);
}
使用 npm
或 yarn
安装 nyc
package,然后在 npm scripts 中加入 coverage,然后运行它 npm run coverage
"scripts": {
"coverage": "nyc --reporter=text-summary node coverage.js"
}
运行完会得到以下结果,可以看到语句覆盖、方法覆盖、行覆盖就是统计被测试代码中有多少条语句、多少个方法、多少行被执行了。比较严格的是分支覆盖,例子中的 flag1 || flag2
会产生四种组合,需要全部覆盖才可以达到分支覆盖率百分之百。
=============================== Coverage summary ===============================
Statements : 87.5% ( 7/8 )
Branches : 50% ( 2/4 )
Functions : 50% ( 1/2 )
Lines : 85.71% ( 6/7 )
================================================================================
通常在没有特殊说明的情况下,描述单元测试覆盖率使用的是语句覆盖。
例子中用到了 nyc
这个包, 它是另一个非常强大的开源库 Instanbul 的 cli 版本。使用 nyc 的配置项 --reporter=lcov --report-dir=./coverage
还可以生成一份测试覆盖率报告的 HTML 文件。
执行环境
如果你从来没写过单元测试,在动手写前端代码的单元测试时可能会感到困惑。就是为什么明明写的是在浏览器跑的代码,却可以用 node 运行起来。
这里指的浏览器跑的代码,包括但不限于以下 WebAPI
:
- document、window 对象
- addEventListener
- appendChild/removeChild
- XMLHttpRequest
- Storage
大方向上看无非就两种方案:
- 用 Node 启动浏览器或无头浏览器来运行前端代码
- 在 Node 中模拟浏览器环境,把所有的 WebAPI 全部模拟实现一遍
第一种方案听起来更加靠谱,然而实际上真实运行起来的速度堪忧,浏览器消耗的内存远大于被测试代码本身。通常在单元测试阶段,我们使用的是第二种方案,在 Node 端使用轻量级的浏览器环境模拟实现。
浏览器的环境模拟库比较受欢迎的是 JSDOM
const assert = require("assert");
const { JSDOM } = require("jsdom");
const { window } = new JSDOM(
`<!DOCTYPE html><body><button id="btn">Buy It</button></body>`
);
const { document } = window;
const button = document.querySelector("#btn");
// Event
button.addEventListener("click", spy);
button.click();
assert(spy.callCount === 1);
assert(spy.args[0][0].target === button);
// DOM API
const block = document.createElement("div");
block.innerHTML = "Block";
document.body.appendChild(block);
assert.deepStrictEqual(
document.body.innerHTML,
'<button id="btn">Buy It</button><div>Block</div>'
);
基于 JSDOM,就可以再 Node 端去模拟一些 DOM 的操作,事件触发等等,它是我们在 Node 端跑前端单元测试的环境基础。可以把上面的代码拷贝一下,创建一个 jsdom.js
文件,使用 npm 后 yarn 安装 jsdom,然后跑起来感受一下。
测试框架
到目前为止,我们的“测试代码”都还是散乱无章的,没有任何约束,也没有办法看到总体的测试结果。这时候测试框架就出现了,它做的事情主要是下面这些:
提供全局的方法、运行骨架,社区的测试框架很多,不过基本用法都差不多。以 mocha 为例,你可以在一个测试文件中,直接使用 describe
关键字来定义一个分组,it
(或 test
) 来定一个具体的单元测试。在分组内还可以使用 before
、after
、beforeEach
、afterEach
等关键字来注册一个钩子方法,它们会分别在组开始、组结束、单个测试开始、单个测试结束时运行。
// 分组
describe("suitcase", () => {
// hooks
before(() => {
console.log("before");
});
after(() => {
console.log("after");
});
beforeEach(() => {
console.log("beforeEach");
});
afterEach(() => {
console.log("afterEach");
});
// 单个单元测试
it("add()", () => {
assert(add(2, 3) === 5);
});
it("addAsync()", async () => {
assert((await addAsync(2, 3)) === 5);
});
});
it
方法包裹了一个具体的测试用例,当这个用例断言失败时,测试框架就会认为这个用例执行失败了。如果需要执行异步代码,加上 async
关键字即可。
组织代码:通常测试框架允许你自定义单元测试代码如何组织,是以 .spec.js
结尾,还是 .test.js
结尾;测试文件放在哪个目录下等等。
执行测试:执行测试是测试框架最基本的任务,它会根据配置约定,从项目中把所有符合条件的测试文件找出来,执行这些文件内的测试代码。
输出执行结果:执行完之后当然就是给出执行结果了,告知开发者哪些测试通过了,哪些不通过。
社区常见的测试框架库有以下这些:
- Jest
- Mocha
- Ava
- ...
结语
这篇文章介绍了单元测试过程中的会出现的各类名词的含义、解答一部分困惑、以及对应分类下社区常见的库都是哪些,它们的基本用法等等。希望看完对你理解单元测试有所帮助。
下期预告:从 Sinon.js 入门测试替身,欢迎点赞+关注。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!