2月阅读周·深入浅出的Node.js | 代码测试,开发者掌握代码的行为和性能的极佳思路
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效。
已读完书籍:《架构简洁之道》。
当前阅读周书籍:《深入浅出的Node.js》。
测试
单元测试
单元测试在软件项目中扮演着举足轻重的角色,是几种软件质量保证的方法中投入产出比最高的一种。
单元测试的意义
开发者写出来的代码是开发者自己的产品。要保证产品的质量,就应该有相应的手段去验证。对于开发者而言,单元测试就是最基本的一种方式。
编写可测试代码有以下几个原则可以遵循:
- 单一职责:如果一段代码承担的职责越多,为其编写单元测试的时候就要构造更多的输入数据,然后推测它的输出。
- 接口抽象:通过对程序代码进行接口抽象后,我们可以针对接口进行测试,而具体代码实现的变化不影响为接口编写的单元测试。
- 层次分离:层次分离实际上是单一职责的一种实现。在MVC结构的应用中,就是典型的层次分离模型,如果不分离各个层次,无法想象这个代码该如何切入测试。通过分层之后,可以逐层测试,逐层保证。
单元测试介绍
单元测试主要包含断言、测试框架、测试用例、测试覆盖率、mock、持续集成等几个方面,由于Node的特殊性,它还会加入异步代码测试和私有方法的测试这两个部分。
1、断言
断言就是单元测试中用来保证最小单元是否正常的检测方法,用于检查程序在运行时是否满足期望。
Node中提供了assert这个模块,用于实现断言。工作方式如下:
var assert = require('assert');
assert.equal(Math.max(1, 100), 100);
2、测试框架
测试框架用于为测试服务,它本身并不参与测试,主要用于管理测试用例和生成测试报告,提升测试用例的开发速度,提高测试用例的可维护性和可读性,以及一些周边性的工作。
推荐单元测试框架:mocha。
3、测试代码的文件组织
包规范中定义了测试代码存在于test目录中,而模块代码存在于lib目录下。
单元测试顺利运行还有个前提:在包描述文件(package.json)中添加相应模块的依赖关系。由于mocha只在运行测试时需要,所以添加到devDependencies节点即可:
"devDependencies": {
"mocha": "*"
}
4、测试用例
一个行为或者功能需要有完善的、多方面的测试用例,一个测试用例中包含至少一个断言。示例代码如下:
describe('#indexOf()', function(){
it('should return -1 when not present', function(){
[1,2,3].indexOf(4).should.equal(-1);
});
it('should return index when present', function(){
[1,2,3].indexOf(1).should.equal(0);
[1,2,3].indexOf(2).should.equal(1);
[1,2,3].indexOf(3).should.equal(2);
});
});
5、测试覆盖率
测试覆盖率是单元测试中的一个重要指标,它能够概括性地给出整体的覆盖度,也能明确地给出统计到行的覆盖情况。
推荐工具:jscover模块。通过npm install jscover -g的方式可以安装该模块。
6、mock
mock即模拟异常,通过伪造被调用方来测试上层代码的健壮性等。
推荐:muk模块。示例代码如下:
var fs = require('fs');
var muk = require('muk');
beforeEach(function () {
muk(fs, 'readFileSync', function(path, encoding) {
throw new Error("mock readFileSync error");
});
});
// it();
// it();
afterEach(function () {
muk.restore();
});
模拟时无须临时缓存正确引用,用例执行结束后调用muk.restore()恢复即可。
7、私有方法的测试
私有方法的测试是单元测试的一个难点。
只有挂载在exports或module.exports上的变量或方法才可以被外部通过require引入访问,其余方法只能在模块内部被调用和访问。
rewire模块提供了一种巧妙的方式实现对私有方法的访问。rewire的调用方式与require十分类似。对于如下的私有方法,我们获取它并为其执行测试用例非常简单:
it('limit should return success', function () {
var lib = rewire('../lib/index.js');
var litmit = lib.__get__('limit');
litmit(10).should.be.equal(10);
});
工程化与自动化
Node以及第三方模块提供的方法都相对偏底层,在开发项目时,还需要一定的工具来实现工程化和自动化,以减少手工成本。
1、工程化
Node在*nix系统下可以很好地利用一些成熟工具,其中Makefile比较小巧灵活,适合用来构建工程。
开发者改动代码之后,只需通过make test和make test-cov命令即可执行复杂的单元测试和覆盖率。
2、持续集成
对于实际的项目而言,频繁地迭代是常见的状态,如何记录版本的迭代信息,还需要一个持续集成的环境。
推荐:利用travis-ci实现持续集成。
性能测试
性能测试包括负载测试、压力测试和基准测试等。
下面主要介绍基准测试,以及如何对Web应用进行网络层面的性能测试和业务指标的换算。
基准测试
基准测试要统计的就是在多少时间内执行了多少次某个方法。为了增强可比性,一般会以次数作为参照物,然后比较时间,以此来判别性能的差距。
这里介绍benchmark这个模块是如何组织基准测试的,相关代码如下:
var Benchmark = require('benchmark');
var suite = new Benchmark.Suite();
var arr = [0, 1, 2, 3, 5, 6];
suite
.add('nativeMap', function () {
return arr.map(callback);
})
.add('customMap', function () {
var ret = [];
for (var i = 0; i < arr.length; i++) {
ret.push(callback(arr[i]));
}
return ret;
})
.on('cycle', function (event) {
console.log(String(event.target));
})
.on('complete', function () {
console.log('Fastest is ' + this.filter('fastest').pluck('name'));
})
.run();
它通过suite来组织每组测试,在测试套件中调用add()来添加被测试的代码。执行上述代码,得到的输出结果如下:
nativeMap x 1,227,341 ops/sec ±1.99% (83 runs sampled)
customMap x 7,919,649 ops/sec ±0.57% (96 runs sampled)
Fastest is customMap
压力测试
对网络接口做压力测试需要考查的几个指标有吞吐率、响应时间和并发数,这些指标反映了服务器的并发处理能力。最常用的工具是ab、siege、http_load等,下面我们通过ab工具来构造压力测试,相关代码如下:
$ ab -c 10-t 3 http://localhost:8001/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 5000 requests
Completed 10000 requests
Finished 11573 requests
Server Software:
Server Hostname: localhost
Server Port: 8001
Document Path: /
Document Length: 10240 bytes
Concurrency Level: 10
Time taken for tests: 3.000 seconds
Complete requests: 11573
Failed requests: 0
Write errors: 0
Total transferred: 119375495 bytes
HTML transferred: 118507520 bytes
Requests per second: 3857.60 [#/sec] (mean)
Time per request: 2.592 [ms] (mean)
Time per request: 0.259 [ms] (mean, across all concurrent requests)
Transfer rate: 38858.59 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 31
Processing: 1 2 1.9 2 35
Waiting: 0 2 1.9 2 35
Total: 1 3 2.0 2 35
Percentage of the requests served within a certain time (ms)
50% 2
66% 3
75% 3
80% 3
90% 3
95% 3
98% 5
99% 6
100% 35 (longest request)
介绍一下各个参数的含义:
- Document Path:表示文档的路径,此处为/。
- Document Length:表示文档的长度,就是报文的大小,这里有10KB。
- Concurrency Level:并发级别,就是我们在命令中传入的c,此处为10,即10个并发。
- Time taken for tests:表示完成所有测试所花费的时间,它与命令行中传入的t选项有细微出入。
- Complete requests:表示在这次测试中一共完成多少次请求。
- Failed requests:表示其中产生失败的请求数,这次测试中没有失败的请求。
- Write errors:表示在写入过程中出现的错误次数(连接断开导致的)。
- Total transferred:表示所有的报文大小。
- HTML transferred:表示仅HTTP报文的正文大小,它比上一个值小。
- Requests per second:这是我们重点关注的一个值,它表示服务器每秒能处理多少请求,是重点反映服务器并发能力的指标。
- Transfer rate:表示传输率,等于传输的大小除以传输时间,这个值受网卡的带宽限制。
- Connection Times:连接时间,它包括客户端向服务器端建立连接、服务器端处理请求、等待报文响应的过程。
基准测试驱动开发
基准测试驱动开发,简称BDD,主要分为以下几个步骤:
(1) 写基准测试。
(2) 写/改代码。
(3) 收集数据。
(4) 找出问题。
(5)回到第(2)步。
测试数据与业务数据的转换
通常,在进行实际的功能开发之前,我们需要评估业务量,以便功能开发完成后能够胜任实际的在线业务量。
如果用户量只有几个,每天的PV只有几十个,那么网站开发几乎不需要什么优化就能胜任。
如果PV上10万甚至百万、千万,就需要运用性能测试来验证是否能够满足实际业务需求,如果不满足,就要运用各种优化手段提升服务能力。
总结
我们来总结一下本篇的主要内容:
- 测试是应用或者系统最重要的质量保证手段。有单元测试实践的项目,必然对代码的粒度和层次都掌握得较好。
- 单元测试能够保证项目每个局部的正确性,也能够在项目迭代过程中很好地监督和反馈迭代质量。
- 对于性能,在编码过程中一定存在部分感性认知,与实际情况有部分偏差,而性能测试则能很好地斧正这种差异。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)