资讯专栏INFORMATION COLUMN

在Nodejs中贯彻单元测试

enali / 2767人阅读

摘要:原文链接在中贯彻单元测试在团队合作中,你写好了一个函数,供队友使用,跑去跟你的队友说,你传个值进去,他就会返回结果了。如果你也为社区贡献过,想更多人使用的话,加上单元测试吧,让你的值得别人信赖。

原文链接:BlueSun | 在Nodejs中贯彻单元测试

在团队合作中,你写好了一个函数,供队友使用,跑去跟你的队友说,你传个A值进去,他就会返回B结果了。过了一会,你队友跑过来说,我传个A值却返回C结果,怎么回事?你丫的有没有测试过啊?

大家一起写个项目,难免会有我要写的函数里面依赖别人的函数,但是这个函数到底值不值得信赖?单元测试是衡量代码质量的一重要标准,纵观Github的受欢迎项目,都是有test文件夹,并且buliding-pass的。如果你也为社区贡献过module,想更多人使用的话,加上单元测试吧,让你的module值得别人信赖。

要在Nodejs中写单元测试的话,你需要知道用什么测试框架,怎么测试异步函数,怎么测试私有方法,怎么模拟测试环境,怎么测试依赖HTTP协议的web应用,需要了解TDD和BDD,还有需要提供测试的覆盖率。

本文的示例代码会备份到 Github : unittest-demo

目录

测试框架

断言库

需求变更

异步测试

异常测试

测试私有方法

测试Web应用

覆盖率

使用Makefile把测试串起来

持续集成,Travis-cli

一些观点

彩蛋

整理

测试框架

Nodejs的测试框架还用说?大家都在用,Mocha。

Mocha 是一个功能丰富的Javascript测试框架,它能运行在Node.js和浏览器中,支持BDDTDDQUnitExports式的测试,本文主要示例是使用更接近与思考方式的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

如果本文对您有用
请不要吝啬你们的Follow与Start
这会大大支持我们继续创作

「Github」
MZMonster :@MZMonster
JC_Huang :@JerryC8080

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78938.html

相关文章

  • 深入贯彻闭包思想,全面理解JS闭包形成过程

    摘要:下面我们就罗列闭包的几个常见问题,从回答问题的角度来理解和定义你们心中的闭包。函数可以通过作用域链相互关联起来,函数内部的变量可以保存在其他函数作用域内,这种特性在计算机科学文献中称为闭包。 写这篇文章之前,我对闭包的概念及原理模糊不清,一直以来都是以通俗的外层函数包裹内层....来欺骗自己。并没有说这种说法的对与错,我只是不想拥有从众心理或者也可以说如果我们说出更好更低层的东西,逼格...

    snowell 评论0 收藏0
  • 测试你的前端代码 - part2(单元测试

    摘要:单元测试上一节有讨论过,单元测试就是以代码单元为单位进行测试,代码单元可以是一个函数,一个模块,或者一个类。单元测试是最容易理解也最容易实现的测试方式。在写单元测试的时候,尽量将你的单元测试独立出来,不要几个单元互相引用。 showImg(https://segmentfault.com/img/remote/1460000008823416?w=997&h=350); 本文作者:G...

    daydream 评论0 收藏0
  • 测试你的前端代码 - part2(单元测试

    摘要:单元测试上一节有讨论过,单元测试就是以代码单元为单位进行测试,代码单元可以是一个函数,一个模块,或者一个类。单元测试是最容易理解也最容易实现的测试方式。在写单元测试的时候,尽量将你的单元测试独立出来,不要几个单元互相引用。 showImg(https://segmentfault.com/img/remote/1460000008823416?w=997&h=350); 本文作者:G...

    shadajin 评论0 收藏0
  • 消息系统设计与实现「上篇」

    摘要:原文链接消息系统设计与实现上篇由于文章篇幅较长,而作者精力有限,不希望这么早就精尽人亡,故分成上下篇来写消息系统的设计与实现。更新于关联文章消息系统设计与实现下篇如果本文对您有用请不要吝啬你们的与这会大大支持我们继续创作 原文链接:Bluesun | 消息系统设计与实现「上篇」 由于文章篇幅较长,而作者精力有限,不希望这么早就精尽人亡,故分成上下篇来写消息系统的设计与实现。上篇主要讲...

    v1 评论0 收藏0
  • 关于前端开发谈谈单元测试

    摘要:很快我发现有一个误区,许多人认为单元测试必须是一个集中运行所有单元的测试,并一目了然。许多人认为单元测试,甚至整个测试都是在编码结束后的一道工序,而修复也不过是在做垃圾掩埋一类的工作。 单元测试Unit Test 很早就知道单元测试这样一个概念,但直到几个月前,我真正开始接触和使用它。究竟什么是单元测试?我想也许很多使用了很久的人也不一定能描述的十分清楚,所以写了这篇文章来尝试描述它...

    0x584a 评论0 收藏0

发表评论

0条评论

enali

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<