Appearance
拓展篇 2-系统稳定性测试 —— 使用 Lab & Code
拓展篇 2:系统稳定性测试 —— 使用 Lab & Code
测试框架,是运行测试的工具。通过它,可以为 JavaScript 应用添加测试,从而保证代码的质量。现行的 Javascript 常用流行测试库有 Jasmine,Mocha, Karma 等,虽然框架的名称不同,但背后的核心套件却大同小异。
以常见的 JavaScript 单元测试框架 Mocha 为例。我们要为一个求和模块 sum
方法来做一个简单的单元测试:
// sum.js
module.exports = (x, y) => x + y
// sum.test.js
const sum = require('./sum.js');
const expect = require('chai').expect;
// 测试套件(test suite) -- describe
describe('求和模块的测试', () => {
// 测试用例(test case) -- it
it('1 加 2 应该等于 3', () => {
// 测试断言(test expect) -- expect
expect(sum(1, 2)).to.be.equal(3);
});
});
由案例中的注视标记可以大致了解到单元测试的三个关键术语:
- describe:测试套件
- it:测试用例
- expect:测试断言
一个完整的测试,可以由多个 describe 组成; 每个 describe 里可以包含多个 describe 或者多个 it ; 每个 it 里可以包涵多个 expect,来最终描述每个测试用例,得到最终的程序执行结果校验。
其他还有一些高级的属性,诸如 before / after / beforeEach / afterEach 等测试用例生命周期的钩子,以及其他特性,我们可以后续再系统学习。
Lab & Code
Lab & Code 是 hapi 的配套测试框架。
Lab
Lab 库支持 async/await,尽可能保持测试引擎的足够简单,并包含了我们希望从现代 Node.js 测试框架程序中需要的所有特性。提供了 describe 和 it,以及生命周期的钩子等功能。
Lab 仅使用 async/await 功能,并包含了你希望从现代 Node.js测试框架程序中需要的所有特性。
Code
Code 库用于提供 expect 断言的相关函数库,code-expect 与 mocha-expect 用法上几乎完全一致。
使用 Lab & Code 测试求和模块
// sum.js
module.exports = (x, y) => x + y
// sum.test.js
var const = require('./sum.js');
// requires for testing
const Code = require('code');
const Lab = require('lab');
const lab = Lab.script();
// 测试框架方法提取
const describe = lab.describe;
const it = lab.it;
const expect = Code.expect;
// 测试套件(test suite) -- describe
describe('求和模块的测试', () => {
// 测试用例(test case) -- it
it('1 加 2 应该等于 3', () => {
// 测试断言(test expect) -- expect
expect(sum(1, 2)).to.equal(3);
});
});
执行单元测试命令 node node_modules/lab/bin/lab \-v
。 -v
的参数打印测试明细,在只想关注错误的测试用例结果时,可以不带上此参数,保持控制台信息输出的简洁性。
# 执行结果
求和模块的测试
✓ 1 加 2 应该等于 3
1 tests complete
Test duration: 236 ms
No global variable leaks detected
在 hapi 中测试 API 接口
以测试用户登录为例,我们希望测试接口的异步调用是否返回 200 的状态编码,并且返回的 response 的 result 中,签发的 JWT payload 中的信息如 userid、openid 符合预期。
hapi 的测试用例中,直接使用 app.inject,即服务器 server 自身的 API,来实现指定接口的调用。代码如下:
// users.test.js
const app = require('../app.js')
// appJWT 用于暂存已登录的用户jwt,供后续需要 jwt 登录认证的测试套件使用
let appJWT = ''
describe("POST: /user/login-jwt-test", () => {
it("状态码200并且返回了正确的jwt", async() => {
const response = await app.inject({
method: 'POST',
url: '/users/login-jwt-test'
})
expect(response.statusCode).to.equal(200)
const JWT = require('jsonwebtoken')
appJWT = response.result
const jwtPayload = JWT.decode(response.result)
expect(jwtPayload.userId).to.equal(1)
expect(jwtPayload.openId).to.equal(1)
})
})
// 获取店铺列表需要用到 authorization 的 jwt 验证,语法实现如下
describe("GET /shops", () => {
it("状态码200", async() => {
const response = await app.inject({
method: 'GET',
url: '/shops',
headers: {
authorization: appJWT
}
})
expect(response.statusCode).to.equal(200)
})
})
hapi 的单元测试框架还会在执行基础测试后,贴心地检测系统中是否存在有全局变量,例如下述的全局变量声明:
foo = 'bar';
hello = 'hapi';
在运行完测试后,会红字提示: The following leaks were detected:foo,hello,引起开发者的重视。
而 No global variable leaks detected 的结果,则表示系统中不存在任何的全局变量,较好地确保了变量作用域的安全可靠性。
常用的断言表达式
// 相等或不相等 equal()
expect(1 + 2).to.be.equal(3);
expect(1 + 2).to.be.not.equal(4);
expect(hello).to.be.deep.equal({ hapi: 'hapi' });
// 布尔值检测 boolean
expect(true).to.be.true();
expect(false).to.not.be.false();
// 数据类型检测 type
expect('hapi').to.be.a.string();
expect({ hello: 'hapi' }).to.be.an.object();
expect(hello).to.be.an.instanceof(Hello);
// 包含检测 include
expect([1,2,3]).to.include(2);
expect('hello hapi').to.contain('hapi');
expect({ hello: 'hapi' }).to.include.keys('hello');
// 判空检测 empty()
expect('').to.be.empty();
expect([]).to.be.empty();
expect({}).to.be.empty();
// 正则匹配 match()
expect('foobar').to.match(/^foo/);
// 断言条件与 and
expect('hello hapi').to.be.a.string().and.contain('hapi');
coverage
coverage 用于量化代码的被测试的比例与程度。被测试的代码数量处以总代码量所得的比例即为覆盖率值。
开启测试覆盖率,可以帮助我们快速定位测试用例所未涵盖的代码区域,决定是否追加测试用例,并且高覆盖率的测试用例,能在一定程度上确保我们的系统基础可执行性。
执行 node node_modules/lab/bin/lab \-v \-c
,追加的 -c
可以额外统计测试用例的覆盖率。覆盖率的测试结果会以比例与未覆盖明细的方式输出显示。
Coverage: 92.26% (57/736)
models/index.js missing coverage on line(s): 35, 41, 49, 50
routes/hello-hapi.js missing coverage on line(s): 8, 9
当然,实际的项目实践中,业务型系统做得越深,代码覆盖率 100% 的实现成本也会越来越高。能效与稳定性之间的平衡点会存在一个分寸的问题,盲目追求 100% 是极度不可取的。我们有几种手法可以帮助优化 coverage 的质量。
1)使用 threshold
针对 coverage,可以设定一个 -t (--threshold)
的参数,来设定一个域值。比如 -t 80
表征覆盖率高于 80% 即满足要求,用来粗颗粒度降低覆盖标准。笔者以为,这样的域值调整意义不大,更多的是一种心理暗示。
2) 使用 --coverage-exclude
/ --coverage-path
--coverage-exclude
与 --coverage-exclude
可以帮助我们直接排除或指定一些目录或是单个文件,来更加明确覆盖率所要覆盖的目标,但精度仅限于文件。一些快速迭代的局部业务级代码,或是非核心主干的代码,可以采用这种方式。
3) 使用 /* $lab:coverage:(off|on)$ */
使用 /* $lab:coverage:(off|on)$ */
可以帮助我们在最终单个文件中的任意代码部分,增加一个特殊约定的表达式,来标记不需要测试覆盖的局部代码,从而提升测试的有效覆盖率。例如:
/* $lab:coverage:off$ */
if (typeof value === 'symbol') {
// do something with value
}
/* $lab:coverage:on$ */
使用 .labrc.js 来进行配置
.labrc.js 是 Lab 的直观配置文件,我们把 .labrc.js 放在当前工程目录的根目录,后续执行 Lab 的指令,将自动从 .labrc.js 中提取相应的运行参数。下面是一个简单的例子等价于执行了 lab \-c \-t 100
。
module.exports = {
coverage: true, //开启覆盖率测试
threshold: 100, //覆盖率搁值为 100%
};
小结
关键词:单元测试,Lab,Code,coverage 覆盖率
本小节,我们介绍了如何使用 Lab & Code,解决测试用例过程中所需要解决的常见问题,从套件定义、用例声明,到最终的断言验证,并通过 coverage 量化测试用例的覆盖率。
基于 REST 接口的开发,一个相对好的习惯是先完成接口文档的书写。利用 hapi-swagger 和 Joi,可以高效地帮助我们完成接口文档化任务。接下来便是书写测试用例,将测试用例通过 BDD 或者 TDD 的方式,进行组合书写,最终得到一套文档完备、测试配套的良好接口。这是一个值得去持续实践优化与体会的好习惯。
本小节参考代码汇总