在Nodejs中贯彻单元测试
在团队合作中,你写好了一个函数,供队友使用,跑去跟你的队友说,你传个A值进去,他就会返回B结果了。过了一会,你队友跑过来说,我传个A值却返回C结果,怎么回事?你丫的有没有测试过啊?
大家一起写个项目,难免会有我要写的函数里面依赖别人的函数,但是这个函数到底值不值得信赖?单元测试是衡量代码质量的一重要标准,纵观Github的受欢迎项目,都是有test文件夹,并且buliding-pass的。如果你也为社区贡献过module,想更多人使用的话,加上单元测试吧,让你的module值得别人信赖。
要在Nodejs中写单元测试的话,你需要知道用什么测试框架,怎么测试异步函数,怎么测试私有方法,怎么模拟测试环境,怎么测试依赖HTTP协议的web应用,需要了解TDD和BDD,还有需要提供测试的覆盖率。
目录
- 测试框架
- 断言库
- 需求变更
- 异步测试
- 异常测试
- 测试私有方法
- 测试Web应用
- 覆盖率
- 使用Makefile把测试串起来
- 持续集成,Travis-cli
- 一些观点
- 彩蛋
- 整理
测试框架
Nodejs的测试框架还用说?大家都在用,Mocha。
Mocha 是一个功能丰富的Javascript测试框架,它能运行在Node.js和浏览器中,支持BDD、TDD、QUnit、Exports式的测试,本文主要示例是使用更接近与思考方式的BDD,如果了解更多可以访问Mocha的官网
测试接口
Mocha的BDD接口有:
- describe()
- it()
- before()
- after()
- beforeEach()
- afterEach()
安装
npm install mocha -g
编写一个稳定可靠的模块
模块具备limit方法,输入一个数值,小于0的时候返回0,其余正常返回
exports.limit = function (num) {
  if (num < 0) {
    return 0;
  }
  return num;
};
目录分配
- lib,存放模块代码的地方
- test,存放单元测试代码的地方
- index.js,向外导出模块的地方
- package.json,包描述文件
测试
var lib = require('index');
describe('module', function () {
  describe('limit', function () {
    it('limit should success', function () {
      lib.limit(10);
    });
  });
});
结果
在当前目录下执行mocha:
$ mocha ․ ✔ 1 test complete (2ms)
断言库
上面的代码只是运行了代码,并没有对结果进行检查,这时候就要用到断言库了,Node.js中常用的断言库有:
- should.js
- expect.js
- chai
加上断言
使用should库为测试用例加上断言
it('limit should success', function () {
  lib.limit(10).should.be.equal(10);
});
需求变更
需求变更啦: limit这个方法还要求返回值大于100时返回100。
针对需求重构代码之后,正是测试用例的价值所在了,
它能确保你的改动对原有成果没有造成破坏。
但是,你要多做的一些工作的是,需要为新的需求编写新的测试代码。
异步测试
测试异步回调
lib库中新增async函数:
exports.async = function (callback) {
  setTimeout(function () {
    callback(10);
  }, 10);
};
测试异步代码:
describe('async', function () {
  it('async', function (done) {
    lib.async(function (result) {
      done();
    });
  });
});
测试Promise
使用should提供的Promise断言接口:
- finally|- eventually
- fulfilled
- fulfilledWith
- rejected
- rejectedWith
- then
测试代码
describe('should', function () {
  describe('#Promise', function () {
    it('should.reject', function () {
      (new Promise(function (resolve, reject) {
        reject(new Error('wrong'));
      })).should.be.rejectedWith('wrong');
    });
    it('should.fulfilled', function () {
      (new Promise(function (resolve, reject) {
        resolve({username: 'jc', age: 18, gender: 'male'})
      })).should.be.fulfilled().then(function (it) {
          it.should.have.property('username', 'jc');
        })
    });
  });
});
异步方法的超时支持
Mocha的超时设定默认是2s,如果执行的测试超过2s的话,就会报timeout错误。
可以主动修改超时时间,有两种方法。
命令行式
mocha -t 10000
API式
describe('async', function () {
  this.timeout(10000);
  it('async', function (done) {
    lib.async(function (result) {
      done();
    });
  });
});
这样的话async执行时间不超过10s,就不会报错timeout错误了。
异常测试
异常应该怎么测试,现在有getContent方法,他会读取指定文件的内容,但是不一定会成功,会抛出异常。
exports.getContent = function (filename, callback) {
  fs.readFile(filename, 'utf-8', callback);
};
这时候就应该模拟(mock)错误环境了
简单Mock
describe("getContent", function () {
  var _readFile;
  before(function () {
    _readFile = fs.readFile;
    fs.readFile = function (filename, encoding, callback) {
      process.nextTick(function () {
        callback(new Error("mock readFile error"));
      });
    };
  });
  // it();
  after(function () {
    // 用完之后记得还原。否则影响其他case
    fs.readFile = _readFile;
  })
});
Mock库
Mock小模块:muk ,略微优美的写法:
var fs = require('fs');
var muk = require('muk');
before(function () {
  muk(fs, 'readFile', function(path, encoding, callback) {
    process.nextTick(function () {
      callback(new Error("mock readFile error"));
    });
  });
});
// it();
after(function () {
  muk.restore();
});
测试私有方法
针对一些内部的方法,没有通过exports暴露出来,怎么测试它?
function _adding(num1, num2) {
  return num1 + num2;
}
通过rewire导出方法
模块:rewire
it('limit should return success', function () {
  var lib = rewire('../lib/index.js');
  var litmit = lib.__get__('limit');
  litmit(10);
});
测试Web应用
在开发Web项目的时候,要测试某一个API,如:/user,到底怎么编写测试用例呢?
使用:supertest
var express = require("express");
var request = require("supertest");
var app = express();
// 定义路由
app.get('/user', function(req, res){
  res.send(200, { name: 'jerryc' });
});
describe('GET /user', function(){
  it('respond with json', function(done){
    request(app)
      .get('/user')
      .set('Accept', 'application/json')
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function (err, res) {
        if (err){
          done(err);
        }
        res.body.name.should.be.eql('jerryc');
        done();
      })
  });
});
覆盖率
测试的时候,我们常常关心,是否所有代码都测试到了。
这个指标就叫做"代码覆盖率"(code coverage)。它有四个测量维度。
- 行覆盖率(line coverage):是否每一行都执行了?
- 函数覆盖率(function coverage):是否每个函数都调用了?
- 分支覆盖率(branch coverage):是否每个if代码块都执行了?
- 语句覆盖率(statement coverage):是否每个语句都执行了?
Istanbul 是 JavaScript 程序的代码覆盖率工具。
安装
$ npm install -g istanbul
覆盖率测试
在编写过以上的测试用例之后,执行命令:
istanbul cover _mocha
就能得到覆盖率:
JerryC% istanbul cover _mocha module
limit
✓ limit should success
async
✓ async
getContent
✓ getContent
add
✓ add should
#Promise
✓ should.reject
✓ should fulfilled 6 passing (32ms) ================== Coverage summary ======================
Statements : 100% ( 10/10 )
Branches : 100% ( 2/2 )
Functions : 100% ( 5/5 )
Lines : 100% ( 10/10 )
==========================================================
这条命令同时还生成了一个 coverage 子目录,其中的 coverage.json 文件包含覆盖率的原始数据,coverage/lcov-report 是可以在浏览器打开的覆盖率报告,其中有详细信息,到底哪些代码没有覆盖到。
上面命令中,istanbul cover 命令后面跟的是 _mocha 命令,前面的下划线是不能省略的。
因为,mocha 和 _mocha 是两个不同的命令,前者会新建一个进程执行测试,而后者是在当前进程(即 istanbul 所在的进程)执行测试,只有这样, istanbul 才会捕捉到覆盖率数据。其他测试框架也是如此,必须在同一个进程执行测试。
如果要向 mocha 传入参数,可以写成下面的样子。
$ istanbul cover _mocha -- tests/test.sqrt.js -R spec
上面命令中,两根连词线后面的部分,都会被当作参数传入 Mocha 。如果不加那两根连词线,它们就会被当作 istanbul 的参数(参考链接1,2)。
使用Makefile串起项目
TESTS = test/*.test.js
REPORTER = spec
TIMEOUT = 10000
JSCOVERAGE = ./node_modules/jscover/bin/jscover test:
@NODE_ENV=test ./node_modules/mocha/bin/mocha -R $(REPORTER) -t $(TIMEOUT) $(TESTS) test-cov: lib-cov
@LIB_COV=1 $(MAKE) test REPORTER=dot
@LIB_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html lib-cov:
@rm -rf ./lib-cov
@$(JSCOVERAGE) lib lib-cov .PHONY: test test-cov lib-cov make test
make test-cov
用项目自身的jscover和mocha,避免版本冲突和混乱
持续集成,Travis-cli
- Travis-ci
- 绑定Github帐号
- 在Github仓库的Admin打开Services hook
- 打开Travis
- 每次push将会hook触发执行npm test命令
 
注意:Travis会将未描述的项目当作Ruby项目。所以需要在根目录下加入.travis.yml文件。内容如下:
language: node_js
node_js:
- "0.12"
Travis-cli还会对项目颁发标签,
or 
如果项目通过所有测试,就会build-passing,
如果项目没有通过所有测试,就会build-failing
一些观点
实施单元测试的时候, 如果没有一份经过实践证明的详细规范, 很难掌握测试的 "度", 范围太小施展不开, 太大又侵犯 "别人的" 地盘. 上帝的归上帝, 凯撒的归凯撒, 给单元测试念念紧箍咒不见得是件坏事, 反而更有利于发挥单元测试的威力, 为代码重构和提高代码质量提供动力.
这份文档来自 Geotechnical, 是一份非常难得的经验准则. 你完全可以以这份准则作为模板, 结合所在团队的经验, 整理出一份内部单元测试准则.
彩蛋
最后,介绍一个库:faker
他是一个能伪造用户数据的库,包括用户常包含的属性:个人信息、头像、地址等等。
是一个开发初期,模拟用户数据的绝佳好库。
支持Node.js和浏览器端。
整理
Nodejs的单元测试工具
- 测试框架 mocha
- 断言库:should.js、expect.js、chai
- 覆盖率:istanbul、jscover、blanket
- Mock库:muk
- 测试私有方法:rewire
- Web测试:supertest
- 持续集成:Travis-cli
参考
- https://github.com/JacksonTian/unittesting
- http://html5ify.com/unittesting/slides/index.html
- http://www.ruanyifeng.com/blog/2015/06/istanbul.html
- http://coolshell.cn/articles/8209.html
- http://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests
- https://github.com/yangyubo/zh-unit-testing-guidelines
- http://www.codedata.com.tw/java/unit-test-the-way-changes-my-programming
- http://wiki.ubuntu.org.cn/%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile:MakeFile%E4%BB%8B%E7%BB%8D
- https://github.com/yangyubo/zh-unit-testing-guidelines
- https://github.com/visionmedia/superagent/blob/master/Makefile
- cnode博客:https://cnodejs.org/topic/55b9e875f36f579657fc52f3
在Nodejs中贯彻单元测试的更多相关文章
- nodejs中获取时间戳、时间差
		Nodejs中获取时间戳的方法有很多种,例如: new Date().getTime() Date.now() process.uptime() process.hrtime() 平时想获取一个时间戳 ... 
- 在Nodejs中如何调用C#的代码
		最近需要在Nodejs中用到C#的代码,从网上了解到可以采用Edgejs来实现Nodejs与C#的代码交互, 直接复制网上的代码运行总是出各种错,填了不少坑,现在把自己的案例代码大致整理一下,方便以后 ... 
- Visual Studio中UnitTesting单元测试模板代码生成
		在软件研发过程中,单元测试的重要性直接影响软件质量.经验表明一个尽责的单元测试方法将会在软件开发的某个阶段发现很多的Bug,并且修改它们的成本也很低.在软件开发的后期阶段,Bug的发 ... 
- nodejs 中自定义事件
		经常看到 req.on('error', function(){...}); 这种代码. 在nodejs中,可以使用 EventEmitter来实现. 具体的关键词有如下几个: var reqEven ... 
- NodeJS中的异步I/O、事件驱动
		nodejs的主要特点是单线程.异步I/O.事件驱动.让我们先大概了解一下这些名词的意思. 单线程 单线程是任务按照顺序执行的,并且每次只执行一个任务,只有前面的任务执行完成以后,后面的任务才执行.在 ... 
- nodejs中Stream的理解
		在nodejs中可以通过fs模块读写文件,我们来看下fs模块提供的接口: fs.readFile(filename, callback) 异步读取文件. filename是读取文件的文件名,如果是相对 ... 
- Nodejs中的this
		以下内容都是关于在nodejs中的this而非javascript中的this,nodejs中的this和在浏览器中javascript中的this是不一样的. 在全局中的this console.l ... 
- 探讨Nodejs中的作用域问题。
		在JS中有全局作用域和函数作用域,而在Nodejs中也自己的作用域,分为全局作用域(global)和模块作用域. js作用域: 以前学js的时候我们的全局对象是window,如: var a = 10 ... 
- nodejs中exports与module.exports的实践
		只要是在nodejs中写自己的文件模块就少不了会遇到module.exports和exports的使用,看别人的代码大多都会使用“module.exports=exports=<对象/函数等&g ... 
随机推荐
- 关于block的回调使用-防止内存泄露问题
			block 一般用于回调,比方请求数据我们把asi封装好,仅仅用block调数据就方便很多 获取到得数据假设要给之加入数据,切记不能够使用self.(这个数组) 或者_(这个数组) addObject ... 
- 给你一个能生成1到5随机数的函数,用它写一个函数生成1到7的随机数。 (即,使用函数rand5()来实现函数rand7())
			给你一个能生成1到5随机数的函数,用它写一个函数生成1到7的随机数. (即,使用函数rand5()来实现函数rand7()). 解答 rand5可以随机生成1,2,3,4,5:rand7可以随机生成1 ... 
- Android Gson解析json详解
			目前解析json有三种工具:org.json(Java常用的解析),fastjson(阿里巴巴工程师开发的),Gson(Google官网出的),解析速度最快的是Gson,下载地址:https://co ... 
- yum安装MangoDB
			1:操作系统信息 2:yum命令查看MongoDB的包信息 3:配置yum源 #vi /etc/yum.repos.d/10gen.repo 4:查看mongoDB的服务器包的信息 5:安装Mongo ... 
- 解决ListView中Item的子控件与Item点击事件冲突
			常常会碰到在ListView中点击当中一个Item.会一并触发其子控件的点击事件.比如Item中的Button.ImageButton等.导致了点击Item中Button以外区域也会触发Button点 ... 
- DB2的认证和授权
			DB2 的安全性由两方面组成:认证和授权 1.认证 认证就是系统验证用户身份的过程.说的简单点,就是验证用户名和密码,因为DB2用户同时也是操作系统用户,所以,首先必须得到操作系统的认可.在默认情况下 ... 
- str.format格式化用法(通过{}来替代%)
			# -*- coding: utf-8 -*- #python 27 #xiaodeng #str.format格式化用法(通过{}来替代%) ''' >>> help(format ... 
- Drupal Form问题汇总
			问:如何校验和提交表单?答:Drupal允许定义默认的表单校验处理函数和提交处理函数. function practice_demo_form($form, &$form_state) { . ... 
- Laravel中的信息验证 和 语言包
			首先,谈下语言包的问题 1.安装语言包,通过composer进行安装 composer require "overtrue/laravel-lang:dev-master" 2.成 ... 
- 【laravel5.4】中jquery的post Ajax提交
			1.post的ajax需要提交csrf_token字段,进行安全过滤 <meta name="csrf-token" content="{{ csrf_token( ... 
