背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。

这个“玩法”虽然常见且板正,但是有效。

已读完书籍《架构简洁之道》。

当前阅读周书籍《深入浅出的Node.js》

测试

单元测试

单元测试在软件项目中扮演着举足轻重的角色,是几种软件质量保证的方法中投入产出比最高的一种。

单元测试的意义

开发者写出来的代码是开发者自己的产品。要保证产品的质量,就应该有相应的手段去验证。对于开发者而言,单元测试就是最基本的一种方式。

编写可测试代码有以下几个原则可以遵循:

  1. 单一职责:如果一段代码承担的职责越多,为其编写单元测试的时候就要构造更多的输入数据,然后推测它的输出。
  2. 接口抽象:通过对程序代码进行接口抽象后,我们可以针对接口进行测试,而具体代码实现的变化不影响为接口编写的单元测试。
  3. 层次分离:层次分离实际上是单一职责的一种实现。在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)步。

阅读周·深入浅出的Node.js | 代码测试,开发者掌握代码的行为和性能的极佳思路_单元测试

测试数据与业务数据的转换

通常,在进行实际的功能开发之前,我们需要评估业务量,以便功能开发完成后能够胜任实际的在线业务量。

如果用户量只有几个,每天的PV只有几十个,那么网站开发几乎不需要什么优化就能胜任。

如果PV上10万甚至百万、千万,就需要运用性能测试来验证是否能够满足实际业务需求,如果不满足,就要运用各种优化手段提升服务能力。

总结

我们来总结一下本篇的主要内容:

  • 测试是应用或者系统最重要的质量保证手段。有单元测试实践的项目,必然对代码的粒度和层次都掌握得较好。
  • 单元测试能够保证项目每个局部的正确性,也能够在项目迭代过程中很好地监督和反馈迭代质量。
  • 对于性能,在编码过程中一定存在部分感性认知,与实际情况有部分偏差,而性能测试则能很好地斧正这种差异。

作者介绍非职业「传道授业解惑」的开发者叶一一。《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。