在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和瀏覽器中,支持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, 是一份非常難得的經驗準則. 你完全可以以這份準則作爲模板, 結合所在團隊的經驗, 整理出一份內部單元測試準則.

單元測試準則
https://github.com/yangyubo/zh-unit-testing-guidelines
彩蛋
*最後,介紹一個庫:faker*

他是一個能僞造用戶數據的庫,包括用戶常包含的屬性:個人信息、頭像、地址等等。

是一個開發初期,模擬用戶數據的絕佳好庫。

支持Node.js和瀏覽器端。
這裏寫圖片描述

生成用戶

整理

**

Nodejs的單元測試工具

**

測試框架 mocha
斷言庫:should.js、expect.js、chai
覆蓋率:istanbul、jscover、blanket
Mock庫:muk
測試私有方法:rewire
Web測試:supertest
持續集成:Travis-cli

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章