帶你入門帶你飛Ⅰ 使用Mocha + Chai + Sinon單元測試Node.js


目錄
1. 簡介
2. 前提條件
3. Mocha入門
4. Mocha實戰
     被測代碼
    Example 1
    Example 2
    Example 3
5. Troubleshooting
6. 參考文檔

 

簡介

Mocha 是具有豐富特性的 JavaScript 測試框架,可以運行在 Node.js 和瀏覽器中,使得異步測試更簡單更有趣。Mocha 可以持續運行測試,支持靈活又准確的報告,當映射到未捕獲異常時轉到正確的測試示例。

Chai 是一個針對 Node.js 和瀏覽器的行為驅動測試和測試驅動測試的斷言庫,可與任何 JavaScript 測試框架集成。

Sinon 是一個獨立的 JavaScript 測試spy, stub, mock庫,沒有依賴任何單元測試框架工程。

 

前提條件

我用的node 和 npm 版本如下:

node -v = v0.12.2

npm -v = 2.7.4

當你成功安裝nodejs 和 npm 后執行如下命令:

npm install -g mocha
npm install sinon
npm install chai

## mocha global 安裝是為了能夠在命令行下面使用命令。

 

Mocha入門

以下為最簡單的一個mocha示例:

var assert = require("assert");
describe('Array', function(){
    describe('#indexOf()', function(){
        it('should return -1 when the value is not present', function(){
            assert.equal(-1, [1,2,3].indexOf(5));
            assert.equal(-1, [1,2,3].indexOf(0));
        })
    })
});
  • describe (moduleName, testDetails)
    由上述代碼可看出,describe是可以嵌套的,比如上述代碼嵌套的兩個describe就可以理解成測試人員希望測試Array模塊下的#indexOf() 子模塊。module_name 是可以隨便取的,關鍵是要讓人讀明白就好。
  • it (info, function)
    具體的測試語句會放在it的回調函數里,一般來說info字符串會寫期望的正確輸出的簡要一句話文字說明。當該it block內的test failed的時候控制台就會把詳細信息打印出來。一般是從最外層的describe的module_name開始輸出(可以理解成沿着路徑或者遞歸鏈或者回調鏈),最后輸出info,表示該期望的info內容沒有被滿足。一個it對應一個實際的test case
  • assert.equal (exp1, exp2)
    斷言判斷exp1結果是否等於exp2, 這里采取的等於判斷是== 而並非 === 。即 assert.equal(1, ‘1’) 認為是True。這只是nodejs里的assert.js的一種斷言形式,下文會提到同樣比較常用的chai模塊。

 

Mocha實戰

項目是基於Express框架的,

項目后台邏輯的層級結構是這樣的 Controller -> model -> lib

文件目錄結構如下

├── config
│   └── config.json
├── controllers
│   └── dashboard
│       └── widgets
│           └── index.js
├── models
│   └── widgets.js
├── lib
│   └── jdbc.js
├── package.json
└── test
    ├── controllers
    │   └── dashboard
    │       └── widgets
    │           └── index_MockTest.js
    ├── models
    │   └── widgetsTest.js
    └── lib
        └── jdbc_mockTest.js

 ##轉載注明出處:http://www.cnblogs.com/wade-xu/p/4665250.html 

 

被測代碼

Controller/dashboard/widgets/index.js

var _widgets = require('../../../models/widgets.js');

module.exports = function(router) {

  router.get('/', function(req, res) {
    _widgets.getWidgets(req.user.id)
            .then(function(widgets){
              return res.json(widgets);
            })
            .catch(function(err){
              return res.json ({
                code: '000-0001',
                message: 'failed to get widgets:'+err
              });
            });
  });
};

 

models/widgets.js    -- functions to get widget of a user from system

var jdbc = require('../lib/jdbc.js');
var Q = require('q');
var Widgets = exports;

/** * Get user widgets * @param {String} userId * @return {Promise} */ Widgets.getWidgets = function(userId) { var defer = Q.defer(); jdbc.query('select * from t_widget A left join t_widget_config B on A.id = B.widgetId where userId =? order by x,y', [userId]) .then(function(rows){ defer.resolve(convertRows(rows)); }).catch(function(err){ defer.reject(err); }); return defer.promise; };

 

lib/jdbc.js  -- function 連接數據庫查詢

var mysql = require('mysql');
var Promise = require('q');
var databaseConfig = require('../config/config.json').database;

var JDBC_MYSQL = exports;

var pool = mysql.createPool({
    connectionLimit: databaseConfig.connectionLimit,
    host: databaseConfig.host,
    user: databaseConfig.user,
    password: databaseConfig.password,
    port: databaseConfig.port,
    database: databaseConfig.database
});

/**
 * Run database query
 * @param  {String} query
 * @param  {Object} [params]
 * @return {Promise}
 */
JDBC_MYSQL.query = function(query, params) {
    var defer = Promise.defer();
    params = params || {};
    pool.getConnection(function(err, connection) {
        if (err) {
            if (connection) {
                connection.release();
            }
            return defer.reject(err);
        }
        connection.query(query, params, function(err, results){
            if (err) {
                if (connection) {
                    connection.release();
                }
                return defer.reject(err);
            }
            connection.release();
            defer.resolve(results);
        });
    });
    return defer.promise;
};

 

config/config.json   --數據庫配置

{
    "database": {
        "host" : "10.46.10.007",
        "port" : 3306,
        "user" : "wadexu",
        "password" : "wade001",
        "database" : "demo",
        "connectionLimit" : 100
    }
}

 ##轉載注明出處:http://www.cnblogs.com/wade-xu/p/4665250.html 

 

Example 1

我們來看如何測試models/widgets.js, 因為是單元測試,所以不應該去連接真正的數據庫, 這時候sinon登場了, stub數據庫的行為,就是jdbc.js這個依賴。

test/models/widgetsTest.js 如下

 1 var jdbc = require('../../lib/jdbc.js');
 2 var widgets = require('../../models/widgets.js');
 3 
 4 var chai = require('chai');
 5 var should = chai.should();
 6 var assert = chai.assert;
 7 
 8 var chaiAsPromised = require('chai-as-promised');
 9 chai.use(chaiAsPromised);
10 
11 var sinon = require('sinon');
12 var Q = require('q');
13 
14 describe('Widgets', function() {
15 
16 
17     describe('get widgets', function() {
18 
19         var stub;
20 
21         function jdbcPromise() {
22             return Q.fcall(function() {
23                 return [{
24                     widgetId: 10
25                 }];
26             });
27         };
28 
29         beforeEach(function() {
30             stub = sinon.stub(jdbc, "query");
31             stub.withArgs('select * from t_widget A left join t_widget_config B on A.id = B.widgetId where userId =? order by x,y', [1]).returns(jdbcPromise());
32 
33         });
34 
35         it('get widgets - 1', function() {
36             return widgets.getWidgets(1).should.eventually.be.an('array');
37         });
38 
39         afterEach(function() {
40             stub.restore();
41         });
42     });
43 });

被測代碼返回的是promise, 所以我們用到了Chai as Promised, 它繼承了 Chai,  用一些流利的語言來斷言 facts about promises.

我們stub住 jdbc.query方法 with 什么什么 Arguments, 然后返回一個我們自己定義的promise, 這里用到的是Q promise

斷言一定要加 eventually, 表示最終的結果是什么。如果你想斷言array里面的具體內容,可以用chai-things, for assertions on array elements.

 

如果要測試catch error那部分代碼,則需要模仿error throwing

 1     describe('get widgets - error', function() {
 2 
 3         var stub;
 4 
 5         function jdbcPromise() {
 6             return Q.fcall(function() {
 7                 throw new Error("widgets error");
 8             });
 9         };
10 
11         beforeEach(function() {
12             stub= sinon.stub(jdbc, "query");
13             stub.withArgs('select * from t_widget A left join t_widget_config B on A.id = B.widgetId where userId =? order by x,y', [1]).returns(jdbcPromise());
14 
15         });
16 
17         it('get widgets - error', function() {
18             return widgets.getWidgets(1).should.be.rejectedWith('widgets error');
19         });
20 
21         afterEach(function() {
22             stub.restore();
23         });
24     });

 

運行測試 結果如下:

 

Example 2

接下來我想測試controller層, 那stub的對象就變成了widgets這個依賴了,

在這里我們用到了supertest來模擬發送http request, 類似功能的模塊還有chai-http

如果我們不去stub,mock 的話也可以,這樣利用supertest 來發送http request 測試controller->model->lib, 每層都測到了, 這就是Integration testing了。

 1 var kraken = require('kraken-js');
 2 var express = require('express');
 3 var request = require('supertest');
 4 
 5 var chai = require('chai');
 6 var assert = chai.assert;
 7 var sinon = require('sinon');
 8 var Q = require('q');
 9 
10 var widgets = require('../../../../models/widgets.js');
11 
12 describe('/dashboard/widgets', function() {
13 
14   var app, mock;
15 
16   before(function(done) {
17     app = express();
18     app.on('start', done);
19 
20     app.use(kraken({
21       basedir: process.cwd(),
22       onconfig: function(config, next) {
23         //some config info, such as login user info in req
24     } }));
25 
26     mock = app.listen(1337);
27 
28   });
29 
30   after(function(done) {
31     mock.close(done);
32   });
33 
34   describe('get widgets', function() {
35 
36     var stub;
37 
38     function jdbcPromise() {
39       return Q.fcall(function() {
40         return {
41           widgetId: 10
42         };
43       });
44     };
45 
46     beforeEach(function() {
47       stub = sinon.stub(widgets, "getWidgets");
48       stub.withArgs('wade-xu').returns(jdbcPromise());
49 
50     });
51 
52     it('get widgets', function(done) {
53       request(mock)
54         .get('/dashboard/widgets/')
55         .expect(200)
56         .expect('Content-Type', /json/)
57         .end(function(err, res) {
58           if (err) return done(err);
59           assert.equal(res.body.widgetId, '10');
60           done();
61         });
62     });
63 
64     afterEach(function() {
65       stub.restore();
66     });
67   });
68 });

注意,it里面用了Mocha提供的done()函數來測試異步代碼,在最深處的回調函數中加done()表示結束測試, 否則測試會報錯,因為測試不等異步函數執行完畢就結束了。

在Example1里面我們沒有用done() 回調函數, 那是因為我們用了Chai as Promised 來代替。

 

運行測試 結果如下:

 ##轉載注明出處:http://www.cnblogs.com/wade-xu/p/4665250.html 

 

Example 3

測試jdbc.js 同理,需要stub mysql 這個module的行為, 代碼如下:

  1 var mysql = require('mysql');
  2 
  3 var databaseConfig = require('../../config/config.json').database;
  4 
  5 var chai = require('chai');
  6 var assert = chai.assert;
  7 var expect = chai.expect;
  8 var should = chai.should();
  9 var sinon = require('sinon');
 10 var Q = require('q');
 11 var chaiAsPromised = require('chai-as-promised');
 12 
 13 chai.use(chaiAsPromised);
 14 
 15 var config = {
 16   connectionLimit: databaseConfig.connectionLimit,
 17   host: databaseConfig.host,
 18   user: databaseConfig.user,
 19   password: databaseConfig.password,
 20   port: databaseConfig.port,
 21   database: databaseConfig.database
 22 };
 23 
 24 describe('jdbc', function() {
 25 
 26   describe('mock query', function() {
 27 
 28     var stub;
 29     var spy;
 30     var myPool = {
 31       getConnection: function(cb) {
 32         var connection = {
 33           release: function() {},
 34           query: function(query, params, qcb) {
 35             var mockQueries = {
 36               q1: 'select * from t_widget where userId =?'
 37             }
 38 
 39             if (query === mockQueries.q1 && params === '81EFF5C2') {
 40               return qcb(null, 'success query');
 41             } else {
 42               return qcb(new Error('fail to query'));
 43             }
 44           }
 45         };
 46         spy = sinon.spy(connection, "release");
 47         cb(null, connection);
 48       }
 49     };
 50 
 51 
 52     beforeEach(function() {
 53       stub = sinon.stub(mysql, "createPool");
 54       stub.withArgs(config).returns(myPool);
 55 
 56     });
 57 
 58     it('query success', function() {
 59       delete require.cache[require.resolve('../../lib/jdbc.js')];
 60       var jdbc = require('../../lib/jdbc.js');
 61       jdbc.query('select * from t_widget where userId =?', '81EFF5C2').should.eventually.deep.equal('success query');
 62       assert(spy.calledOnce);
 63     });
 64 
 65     it('query error', function() {
 66       delete require.cache[require.resolve('../../lib/jdbc.js')];
 67       var jdbc = require('../../lib/jdbc.js');
 68       jdbc.query('select * from t_widget where userId =?', 'WrongID').should.be.rejectedWith('fail to query');
 69       assert(spy.calledOnce);
 70     });
 71 
 72     afterEach(function() {
 73       stub.restore();
 74       spy.restore();
 75     });
 76 
 77   });
 78 
 79   describe('mock query error ', function() {
 80 
 81     var stub;
 82     var spy;
 83 
 84     var myPool = {
 85       getConnection: function(cb) {
 86         var connection = {
 87           release: function() {},
 88         };
 89         spy = sinon.spy(connection, "release");
 90         cb(new Error('Pool get connection error'));
 91       }
 92     };
 93 
 94     beforeEach(function() {
 95       stub = sinon.stub(mysql, "createPool");
 96       stub.withArgs(config).returns(myPool);
 97     });
 98 
 99     it('query error without connection', function() {
100       delete require.cache[require.resolve('../../lib/jdbc.js')];
101       var jdbc = require('../../lib/jdbc.js');
102       jdbc.query('select * from t_widget where userId =?', '81EFF5C2').should.be.rejectedWith('Pool get connection error');
103 
104       assert.isFalse(spy.called);
105     });
106 
107     afterEach(function() {
108       stub.restore();
109       spy.restore();
110     });
111 
112   });
113 
114 });

這里要注意的是我每個case里面都是 delete cache 不然只有第一個case會pass, 后面的都會報錯, 后面的case返回的myPool都是第一個case的, 因為第一次create Pool之后返回的 myPool被存入cache里了。

 

測試運行結果如下

 ##轉載注明出處:http://www.cnblogs.com/wade-xu/p/4665250.html 

 

Troubleshooting

1. stub.withArgs(XXX).returns(XXX) 這里的參數要和stub的那個方法里面的參數保持一致。

2. stub某個對象的方法 還有onFirstCall(), onSecondCall() 做不同的事情。

3. 文中提到過如何 remove module after “require” in node.js 不然創建的數據庫連接池pool一直在cache里, 后面的case無法更改它.

   delete require.cache[require.resolve('../../lib/jdbc.js')];

4. 如何引入chai-as-promised

var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);

5. mocha無法命令行運行,設置好你的環境變量PATH路徑 

 

參考文檔

Mocha: http://mochajs.org/

Chai: http://chaijs.com/

Sinon: http://sinonjs.org/

 

感謝閱讀,如果您覺得本文的內容對您的學習有所幫助,您可以點擊右下方的推薦按鈕,您的鼓勵是我創作的動力。

##轉載注明出處:http://www.cnblogs.com/wade-xu/p/4665250.html 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM