我正在参加「掘金·启航计划」
从零开始的中大厂前端项目落地(三)
落地一个前端项目涉及非常多知识,因此从6个角度入手,化整为零,逐个击破:
- 选择合适的构建工具
- 定制团队代码规范
- 测试工具
- 封装自己的组件
- 自动部署
- 安全
如何做前端测试
测试可以包含多个维度,如单元测试、兼容性测试、黑盒测试等。本章主要介绍单元测试,测试驱动开发可以帮助开发者找到遗漏的逻辑从而更好地完成开发,同时在后续迭代中也能保证不会引入缺陷。
随着代码行数的增多,Bug 在所难免,而避免 Bug 最好的方法就是进行测试对于库来说,每次改动代码都要进行全面的测试。特别是当要对项目代码进行重构时测试能够降低重构的风险。但是,如果每次都通过人工进行测试,则既浪费时间又容易出错,更好的做法是编写代码来测试代码,因为代码能够快速多次运行,并且稳定可靠,这种方法被称作单元测试。
对核心组件覆盖自动化测试,可以有效地保证组件功能的单一,起到警醒工程师的作用,而不至于让不同的业务代码相互耦合;新同学可以通过单测快速 get 到这个组件打算做什么、有什么能力,不论是后续的维护还是重构都会更有底气。
通过测试来保证和提升代码质量。设计单元测试用例的方法有两种,分别是测试驱动开发(Test DrivenDevelopment,TDD)和行为驱动开发 (Behavior Driven Development,BDD)。
JavaScript 中的单元测试有很多技术方案,每种方案都有自己的优点和适应场景。类似 React 之类的框架都提供了默认的测试方案,如果要写 React 的项目,那么直接使用框架推荐的测试方案即可。下图是双越老师在某个课程里面写的测试代码:
对于通用的基础建设,相比手工测试,自动化测试的覆盖率更有说服力,并且可以有效规避某次修改引起的历史功能的异常,从而保证整体功能的稳定。
想必大家也知道自动化测试的重要性。其实,对于前端工程师而言,最大的痛点在于,不知道该怎么去写对应的测试用例。而如何在项目中使用测试代码,将通过以下点讲述:
- 测试工具的选型
- Jest
- 设计测试用例
测试工具的选型
Mocha
Mocha 是一个功能丰富的 JavaScript 测试框架,运行在 Node.js 和 浏览器 中,使异步测试变得简单而有趣。Mocha 也是历史比较悠久的测试框架,其相对比较成熟,并且使用范围广泛,兼容性能够满足我们的要求。虽然 Mocha 可以提供组织和运行单元测试并输出测试报告的功能,但是要进行单元测试还需要一个断言库,Mocha 推荐使用 Chai 作为断言库。由于 Chai 不能够兼容 IE8 浏览器,因这里使用另一个断言库--expet.js。expect.js 是一个 BDD 体系的断言库,兼容性非常好,甚至可以支持 IE6 浏览器。
接下来我们在项目中安装一下 Mocha,注意:Mocha (version:10+)需要 Node (version:14+)
npm install --save-dev mocha
然后在文件夹添加一个测试用的 js 文件:
var assert = require('assert');
describe('Array', function () {
describe('#indexOf()', function () {
it('should return -1 when the value is not present', function () {
assert.equal([1, 2, 3].indexOf(4), -1);
});
});
});
这里的 assert 需要 require 一下,然后再打开命令行工具,通过下面的命令执行测试。其中,npx 前缀表示寻找当前路径下的 node modules 目录下的 mocha 命令并执行,如果不使用 npx,则需要通过路径来引用。下面三种命令的效果是等价的,推荐使用 npm run 方式来执行。
// 直接用npx运行
npx mocha
// 通过bin运行
./node_modules/mocha/bin/mocha
// 在package.json添加scripts运行
npm run test
可以看到我们刚刚写的测试已经 passing,即代表我们的测试通过了。
Jest
Jest因为只带了 expect,所以在写测试的时候不用额外引入断言了。我们在项目里面试一下吧,这一次我们建一个 js工具库,这个 sum.js简单的求和工具。
npm install --save-dev jest
function sum(a, b) {
return a + b;
}
module.exports = sum;
然后我们开始编写测试代码,新建一个 sum.test.js文件:
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
然后再运行一下,也是 pass 了
Jest也能在 vscode 上使用, vscode-jest,这个插件能自动识别 vscode 上的项目,并且可以显示代码通过率。
使用Jest
通过上面的 Jest 示例,可以发现测试代码调用了 expect 方法,expect 接收一个参数,输入参数即要测试的对象,如 sum 函数。接着通过链式调用 toBe ,toBe 用于比较原始值或检查对象实例的引用标识。它调用Object.is比较值,这比严格的相等运算符更适合测试===。
回到上面的例子中,sum 输入 1 和 2,期望 sum 处理后输出 3,那么我们就在测试文件写上 .toBe(3),接下来就用程序自己去跑就可以了。
用断言的使用场景可以分成以下六个方向:
基础类型的比较
| 涉及的断言Api | 解析 | 示例 |
|---|---|---|
| not | 用来表示非的判断 | expect(1 + 1).not.toBe(3); |
| toBe(value) | 基础类型的判断 | expect(true).toBe(true); |
| toBeTruthy(value) | 六个假值:false、0、''、null、undefined和NaN。其他一切都为真 | expect(true).toBeTruthy(); |
| toBeFalsy(value) | 和toBeTruthy相反 | expect(true).toBeFalsy(); |
| toBeDefined() | 检查变量是否未定义 | expect(undefined).not.toBeDefined(); |
| toBeUndefined() | 检查变量是否未定义 | expect(undefined).toBeUndefined(); |
| toBeCloseTo(value) | 比较浮点数是否近似相等 | expect(0.2 + 0.1).toBeCloseTo(0.3); |
| toBeNaN() | 检查值是否为NaN | expect(NaN).toBeNaN(); |
引用类型的比较
| 涉及的断言Api | 解析 | 示例 |
|---|---|---|
| toEqual(value) | 深度递归对象的每个属性,进行深度比较,只要原始值相同,那就可以通过断言 | expect({ obj1: { name: "obj1" } }).toEqual(Object.assign({ obj1: { name: "obj1" } })); |
数字符号
| 涉及的断言Api | 解析 | 示例 |
|---|---|---|
| toEqual(value) | 解析 | |
| toBeLessThan(value) | 大于某个整数(received > expected) | expect(3).toBeGreaterThan(2); |
| toBeGreaterThanOrEqual(value) | 大于等于某个整数(received >= expected) | expect(3).toBeGreaterThanOrEqual(3); |
| toBeLessThanOrEqual(value) | 小于于某个整数(received <= expected) | expect(3).toBeLessThanOrEqual(4); |
正则匹配
| 涉及的断言Api | 解析 | 示例 |
|---|---|---|
| toMatch(value) | 检查字符串是否与正则表达式匹配 | expect("This is a regexp validation").toMatch(/regexp/); |
| toMatchObject(value) | 验证对象能否包含 value 的全部属性,即 value 是否是匹配对象的子集 |
const obj = { prop1: "test", prop2: "regexp validation" };
const childObj = { prop1: "test" }; expect(obj).toMatchObject(childObj); |
表单验证
| 涉及的断言Api | 解析 | 示例 |
|---|---|---|
| toContain(value) | 判定某个值是否存在在数组中 | expect([1, 2, 3]).toContain(1); |
| arrayContaining(value) | 匹配接收到的数组,与 toEqual 结合使用可以用于判定某个数组是否是另一个数组的子集 | expect([1, 2, 3]).toEqual(expect.arrayContaining([1, 2])); |
| toContainEqual(value) | 用于判定某个对象元素是否在数组中 | expect([{ a: 1, b: 2 }]).toContainEqual({ a: 1, b: 2 }); |
| toHaveLength(value) | 断言数组的长度 | expect([1, 2, 3]).toHaveLength(3); |
| toHaveProperty(value) | 断言对象中是否包含某个属性,针对多层级的对象可以通过 xx.yy 的方式进行传参断言 | expect(testObj).toHaveProperty("prop1"); |
错误抛出
| 涉及的断言Api | 解析 | 示例 |
|---|---|---|
| toThrow() | 测试函数在被调用时是否抛出 | expect(throwError).toThrow(); |
| toThrowError() | 测试函数在被调用时是否抛出 | expect(throwError).toThrowError(); |
设计测试用例 && 覆盖率指标
在编写代码之前需要先设计测试用例,测试用例要尽可能全面地覆盖各种情况,这样才能保证质量;在覆盖全面的同时,数量要尽可能少,这样能够提高测试效率。对于函数的测试,可以按照参数分组,每个参数一组,在对一个参数进行测试时,保证其他参数无影响。对于存在边界值情况的参数,还需要对边界值设计测试用例。
在编写单元测试时,如何保证所有代码都能够被测试到呢?设计测试用例的方法基本可以保证主流程的测试,但依然存在人为的疏忽和一些边界情况可能漏测的问题。代码覆盖率是衡量测试是否严谨的指标,检查代码覆盖率可以帮助单元测试查漏补缺。
覆盖率指标指的是测试用例覆盖文件的质量指标,通常指以下4个
| 指标名称 | 指标内容 |
|---|---|
| statement | 语句覆盖率,是不是每个语句都执行了 |
| branch | 分支覆盖率,是不是每个 if 判断都执行了 |
| function | 函数覆盖率,是不是每个函数都执行了 |
| line | 行覆盖率,是不是每行都执行了 |
小结
本章介绍了单元测试相关的整套方案,其中涉及不少工具,读者不必一次掌握,配置好环境后关注自己的测试用例即可。此外,目前单元测试领域有了一些更新的技术值得关注,包括但不限于单元测试框架 Jest 和 UI 自动化测试框架 Cypress。在测试不同的库时可以用不同的测试方案,但单元测试是保证质量必不可少的流程,设计良好的测试用例和检查代码覆盖率是保证测试质量的方法。
最后,让我们一起加油吧!
参考资料: