天天看點

Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率

——————————————————————————————————————

單元測試Express/NodeJs

個人了解,

1,如果不是測試http請求的單元測試,用Mocha, Chai等基本夠用了,因為可以直接調用測試函數或者方法,傳進輸入參數值,函數執行完傳回輸入,對輸出斷言即可,即可以對此函數獨立進行測試。并且可以用istanbul和mocha-lcov-reporter出測試覆寫率報告,也可以出html的形式的報告

另外,如果此函數中需要調用網絡上或者其他依賴的api,可能需要mock此api的輸入輸出,此時可用到Nock包做mock相關的事情。

Sample:

https://github.com/schulzetenberg/nock-test/blob/master/index.js

https://github.com/schulzetenberg/nock-test/blob/master/test/index.spec.js

//被測試的函數

/*jshint esversion: 6 */

var Q = require('q');
var Client = require('node-rest-client').Client;

exports.getUserFollowers = function(username) {
  var defer = Q.defer();

  var client = new Client();

           
//此處需要被mock
  var request = client.get(`https://api.github.com/users/${username}/followers`, function(data, response) {
    defer.resolve(data);
  });

  request.on('requestTimeout', function(req) {
    req.abort();
    defer.reject("Request has expired");
  });

  request.on('responseTimeout', function(res) {
    defer.reject("Response has expired");
  });

  request.on('error', function(err) {
    defer.reject("Request error", err.request.options);
  });

  return defer.promise;
};
           

//單元測試,用nock做mock

var expect = require('chai').expect;
var nock = require('nock');
var getUserFollowers = require('../index').getUserFollowers;

describe('GET followers', function() {
  beforeEach(function() {
    var followersResponse = [{
      "login": "octocat",
      "id": 583231,
      "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=3",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    }, {
      "login": "nanocat",
      "id": 583233,
      "avatar_url": "https://avatars.githubusercontent.com/u/583233?v=3",
      "gravatar_id": "",
      "url": "https://api.github.com/users/nanocat",
      "html_url": "https://github.com/nanocat",
      "followers_url": "https://api.github.com/users/nanocat/followers",
      "following_url": "https://api.github.com/users/nanocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/nanocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/nanocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/nanocat/subscriptions",
      "organizations_url": "https://api.github.com/users/nanocat/orgs",
      "repos_url": "https://api.github.com/users/nanocat/repos",
      "events_url": "https://api.github.com/users/nanocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/nanocat/received_events",
      "type": "User",
      "site_admin": false
    }];

    // Mock the TMDB configuration request response
    nock('https://api.github.com')
      .get('/users/octocat/followers')
      .reply(200, followersResponse);
  });

  it('returns users followers', function() {

    var username = 'octocat';

    return getUserFollowers(username).then(function(followers) {
      // It should return an array object
      expect(Array.isArray(followers)).to.equal(true);
      // Ensure that at least one follower is in the array
      expect(followers).to.have.length.above(1);
    });

  });
});
           

本地做測試時,用完nock做完mock後,在請求nock設定的url,隻有用http.get(){ res.on('data') ... res.once('end')},現在on data裡接收資料,之後在end裡取 on data裡接收整理過的資料才好用,用request包或者其他的方式都不好用,不知道為什麼,可能是相容性問題

Nock: https://github.com/node-nock/nock

var req = http.get(`${host_port}/xxx/xxx?a=a1`, function(res) {
		res.setEncoding('utf8');
		var body = '';
		// console.log("http.get res nock nocknock res: " + JSON.stringify(res));
		res.on('data', function(chunk) {
				body += chunk;
			})
			.once('end', function() {
				// console.log("http.get res nock body once end : " + body);
				expect(JSON.parse(body).status).to.equal(httpStatus.OK);
				scope.done();
				done();
			});
	});
           

2,如果測試http請求的request/response,就需要對req/res做mock,可以用node-mocks-http,測試過可行。

另一個沒有驗證的mock架構mock-express:https://www.npmjs.com/package/mock-express

//sample

Workshop - 對Express中間件進行單元測試:http://blog.leapoahead.com/2015/09/09/unittesting-express-middlewares/

tjwudi/unit-testing-express-middlewares-example:https://github.com/tjwudi/unit-testing-express-middlewares-example

//另一個很不錯的sample,比較全面

How To Test Your Express Controllers:https://www.terlici.com/2015/09/21/node-express-controller-testing.html

https://github.com/howardabrams/node-mocks-http

——————————————————————————————————————

---------------------------------------------

//先了解一下nodejs的單元測試

Node.js 單元測試:我要寫測試: 

http://taobaofed.org/blog/2015/12/10/nodejs-unit-tests/

nodejs單元測試ppt:

http://html5ify.com/unittesting/slides/#/1

---------------------------------------------

Mocha單元測試簡介:

http://unitjs.com/guide/mocha.html

Mocha官網和Github:

https://mochajs.org/

NodeJs測試架構Mocha的簡單介紹:http://blog.csdn.net/leoleocs/article/details/50016263

https://github.com/mochajs/mocha

測試架構mochajs詳解:

http://www.cnblogs.com/Leo_wl/p/5734889.html

測試架構 Mocha 執行個體教程《作者: 阮一峰》:

http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html

javascript單元測試架構mochajs詳解《了解api》:

http://www.cnblogs.com/tzyy/p/5729602.html#_h1_56

Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率:

https://cnodejs.org/topic/558df089ebf9c92d17e73358

wx 是一個不錯的微信應用架構,接口和網站做的也不錯,和wechat-api是類似的項目

群裡有人問哪個好

樸靈說:“不寫測試的項目都不是好項目”

确實wx目前還沒有測試,對于一個開源項目來說,沒有測試和代碼覆寫率是不完善的,而且從技術選型來說,大多是不敢選的。

那麼Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率呢?

測試

目前主流的就bdd和tdd,自己查一下差異

推薦

  • mocha和tape

另外Jasmine也挺有名,angularjs用它,不過挺麻煩的,還有一個選擇是qunit,最初是為jquery測試寫的,在nodejs裡用還是覺得怪怪的。

如果想簡單可以tap,它和tape很像,下文會有詳細說明

mocha

mocha是tj寫的

https://github.com/mochajs/mocha

var assert = require("assert")
describe('truth', function(){
  it('should find the truth', function(){
    assert.equal(1, 1);
  })
})
           

斷言風格,這裡預設是assert,推薦使用chaijs這個子產品,它提供3種風格

  • Should
  • Expect
  • Assert

rspec裡推薦用expect,其實看個人習慣

比較典型一個mocha例子

var assert = require('chai').assert;
var expect = require('chai').expect;
require('chai').should();


describe('Test', function(){
	before(function() {
    // runs before all tests in this block
	
  })
  after(function(){
    // runs after all tests in this block
  })
  beforeEach(function(){
    // runs before each test in this block
  })
  afterEach(function(){
    // runs after each test in this block
  })

  describe('#test()', function(){
    it('should return ok when test finished', function(done){
      assert.equal('sang_test2', 'sang_test2');
      var foo = 'bar';
      expect(foo).to.equal('bar');
      done()
    })
  })
})
           

說明

  • 了解測試生命周期
  • 了解bdd測試寫法

單元測試需要的各個子產品說明

  • mocha(Mocha is a feature-rich JavaScript test framework running on node.js and the browser, making asynchronous testing simple and fun.)
  • chai(Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.)
  • sinon(Standalone test spies, stubs and mocks for JavaScript.)
  • zombie (頁面事件模拟Zombie.js is a lightweight framework for testing client-side JavaScript code in a simulated environment. No browser required.)
  • supertest(接口測試 Super-agent driven library for testing node.js HTTP servers using a fluent API)

更多的看 http://nodeonly.com/2014/11/24/mongoose-test.html

如果你想真正的玩靈活,從使用者故事開始,那麼下面這2個庫非常必要

  • http://vowsjs.org/
  • https://github.com/cucumber/cucumber-js

啊,黃瓜。。。。

tape:像代碼一樣跑測試

tape是substack寫的測試架構

https://github.com/substack/tape

var test = require('tape').test;
test('equivalence', function(t) {
    t.equal(1, 1, 'these two numbers are equal');
    t.end();
});
           

tape是非常簡單的測試架構,核心價值觀是”Tests are code”,是以你可以像代碼一樣跑測試,

比如

寫個腳本就無比簡單了。當然如果你想加’test runner’ 庫也有現成的。

The Test Anything Protocol

TAP全稱是Test Anything Protocol

它是可靠性測試的一種(tried & true)實作

從1987就有了,有很多語言都實作了。

它說白點就是用賊簡單的方式來格式化測試結果,比如

TAP version 13
# equivalence
ok 1 these two numbers are equal

1..1
# tests 1
# pass  1

# ok
           

比如node裡的實作https://github.com/isaacs/node-tap

var tap = require('tap')

// you can test stuff just using the top level object.
// no suites or subtests required.

tap.equal(1, 1, 'check if numbers still work')
tap.notEqual(1, 2, '1 should not equal 2')

// also you can group things into sub-tests.
// Sub-tests will be run in sequential order always,
// so they're great for async things.

tap.test('first stuff', function (t) {
  t.ok(true, 'true is ok')
  t.similar({a: [1,2,3]}, {a: [1,2,3]})
  // call t.end() when you're done
  t.end()
})
           

一定要區分tap和tape,不要弄混了

科普一下什麼是CI

科普一下,CI = Continuous integration 持續內建

Martin Fowler對持續內建是這樣定義的:

持續內建是一種軟體開發實踐,即團隊開發成員經常內建他們的工作,通常每個成員每天至少內建一次,也就意味着每天可能會發生多次內建。每次內建都通過自動化的建構(包括編譯,釋出,自動化測試)來驗證,進而盡快地發現內建錯誤。許多團隊發現這個過程可以大大減少內建的問題,讓團隊能夠更快的開發内聚的軟體。

它可以

  • 減少風險
  • 減少重複過程
  • 任何時間、任何地點生成可部署的軟體
  • 增強項目的可見性
  • 建立團隊對開發産品的信心

要素

1.統一的代碼庫2.自動建構3.自動測試4.每個人每天都要向代碼庫主幹送出代碼5.每次代碼遞交後都會在持續內建伺服器上觸發一次建構6.保證快速建構7.模拟生産環境的自動測試8.每個人都可以很容易的擷取最新可執行的應用程式9.每個人都清楚正在發生的狀況10.自動化的部署

也就是說,測試不通過不能部署,隻有送出到伺服器上,就可以自動跑測試,測試通過後,就可以部署到伺服器上了(注意是"staging", 而非"production")。

一般最常的ci軟體是jenkins

舉個大家熟悉的例子iojs開發中的持續內建就是用的jenkins

https://jenkins-iojs.nodesource.com/

Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率

jenkins是自建環境下用的比較多,如果是開源項目,推薦travis-ci

https://travis-ci.org/

對開源項目做持續內建是免費的(非開源的好貴),是以在github內建的基本是最多的。

對nodejs支援的也非常好。

舉2個例子

  • express https://travis-ci.org/strongloop/express
  • koa https://travis-ci.org/koajs/koa

測試報告

近年随着tdd/bdd,開源項目,和靈活開發的火熱,程式員們不再滿足說,我貢獻了一個開源項目

要有高要求,我要加測試

要有更高要求,我要把每一個函數都測試到,讓别人相信我的代碼沒有任何問題

上一小節講的ci,實際上解決了反複測試的自動化問題。但是如何看我的程式裡的每一個函數都測試了呢?

答案是測試覆寫率

在nodejs裡,推薦istanbul

Istanbul - 官方介紹 a JS code coverage tool written in JS

它可以通過3種途徑生成覆寫報告

  • cli
  • 代碼
  • gulp插件

安裝

執行

它會生成

./coverage

目錄,這裡面就是測試報告

比如我的項目裡

./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly
    #MongooseDao()
      ✓ should return ok when record create
      ✓ should return ok when record delete fixture-user
      ✓ should return ok when record deleteById
      ✓ should return ok when record removeById
      ✓ should return ok when record getById
      ✓ should return ok when record getAll
      ✓ should return ok when record all
      ✓ should return ok when record query


  8 passing (50ms)

=============================================================================
Writing coverage object [/Users/sang/workspace/moa/mongoosedao/coverage/coverage.json]
Writing coverage reports at [/Users/sang/workspace/moa/mongoosedao/coverage]
=============================================================================

=============================== Coverage summary ===============================
Statements   : 47.27% ( 26/55 )
Branches     : 8.33% ( 1/12 )
Functions    : 60% ( 9/15 )
Lines        : 47.27% ( 26/55 )
================================================================================
           

預設,它會生成coverage.json和Icov.info,如果你想生成html也可以的。

比如說,上面的結果47.27%是我測試覆寫的占比,即55個函數,我的測試裡隻覆寫了26個。

那麼我需要有地方能夠展示出來啊

實踐

我們以mongoosedao項目為例,介紹一下如何內建測試,ci和測試覆寫率

最終效果如圖

Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率

npm run

package.json裡定義自定義執行腳本

"scripts": {
  "start": "npm publish .",
  "test": "./node_modules/.bin/gulp",
  "mocha": "./node_modules/.bin/mocha -u bdd",
  "cov":"./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
},
           

除了start和test外,都是自定義任務,其他都要加run指令

npm run mocha
npm run cov
           

更多見npm-run-test教程

gulp watch

var gulp = require('gulp');
var watch = require('gulp-watch');

var path = 'test/**/*.js';

gulp.task('watch', function() {
  gulp.watch(['test/**/*.js', 'lib/*.js'], ['mocha']);
});

var mocha = require('gulp-mocha');

gulp.task('mocha', function () {
    return gulp.src(path , {read: false})
        // gulp-mocha needs filepaths so you can't have any plugins before it 
        .pipe(mocha({reporter: 'spec'}));
});


gulp.task('default',['mocha', 'watch']);
           

這樣就可以執行gulp的時候,當檔案變動,會自動觸發mocha測試,簡化每次都輸入npm test這樣的操作。

當然你可以玩更多的gulp,如果不熟悉,參考

  • 介紹gulp的一張不錯的圖
  • gulp實踐

建立

.travis.yml

項目根目錄下,和package.json平級

language: node_js
repo_token: COVERALLS.IO_TOKEN
services: mongodb
node_js:
  - "0.12"
  - "0.11"
  - "0.10"
script: npm run mocha
after_script:
  npm run cov
           

說明

  • 如果依賴mongo等資料庫,一定要寫services
  • 把測試覆寫率放到執行測試之後,避免報402錯誤

在travis-ci.org上,github授權,添加repo都比較簡單

添加之後,就可以看到,比如

https://travis-ci.org/moajs/mongoosedao

travis-ci實際上根據github的代碼變動進行自動持續建構,但是有的時候它不一定更新,或者說,你需要手動選一下:

Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率

點選

# 10 passed

,這樣就可以強制它手動內建了。

其他都很簡單,注意替換COVERALLS.IO_TOKEN即可。

建立 

.coveralls.yml

https://coveralls.io/是一個代碼測試覆寫率的網站,

nodejs下面的代碼測試覆寫率,原理是通過istanbul生成測試資料,上傳到coveralls網站上,然後以badge的形式展示出來

比如

Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率

具體實踐和travis-ci類似,用github賬号登陸,然後添加repo,然後在項目根目錄下,和package.json平級,增加

.coveralls.yml

service_name: travis-pro
repo_token: 99UNur6O7ksBqiwgg1NG1sSFhmu78A0t7
           

在上,第一次添加repo,顯示的是“SET UP COVERALLS”,裡面有token,需要放到

.coveralls.yml

裡,

Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率

如果成功送出了,就可以看到資料了

Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率

在readme.md裡增加badge

[![Build Status](https://travis-ci.org/moajs/mongoosedao.png?branch=master)](https://travis-ci.org/moajs/mongoosedao)
[![Coverage Status](https://coveralls.io/repos/moajs/mongoosedao/badge.png)](https://coveralls.io/r/moajs/mongoosedao)
           

它就會顯示如下

Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率
Node.js 單元測試:我要寫測試 - Mocha - Nodejs開源項目裡怎麼樣寫測試、CI和代碼測試覆寫率

另外一種用Makefile的玩法實踐

舉例:https://github.com/node-webot/wechat-api/blob/master/Makefile

TESTS = test/*.js
REPORTER = spec
TIMEOUT = 20000
ISTANBUL = ./node_modules/.bin/istanbul
MOCHA = ./node_modules/mocha/bin/_mocha
COVERALLS = ./node_modules/coveralls/bin/coveralls.js

test:
	@NODE_ENV=test $(MOCHA) -R $(REPORTER) -t $(TIMEOUT) \
		$(MOCHA_OPTS) \
		$(TESTS)

test-cov:
	@$(ISTANBUL) cover --report html $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS)

test-coveralls:
	@$(ISTANBUL) cover --report lcovonly $(MOCHA) -- -t $(TIMEOUT) -R spec $(TESTS)
	@echo TRAVIS_JOB_ID $(TRAVIS_JOB_ID)
	@cat ./coverage/lcov.info | $(COVERALLS) && rm -rf ./coverage

test-all: test test-coveralls

.PHONY: test
           

我個人更喜歡npm+gulp的寫法,總是有一種make是c裡古老的東東。。。

總結

本文講了

  • nodejs裡常用架構
    • mocha
    • tape
    • tap
    • 前沿技術:cucumber和vowsjs
  • 科普一下CI
  • 測試報告
    • istanbul
  • 實踐
    • gulp + npm run
    • mocha
    • travis-ci
    • coveralls
  • 介紹了基于makefile的另一種玩法

全文完