hyytaojunming 发表于 2017-2-24 08:08:08

带你入门带你飞Ⅰ 使用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, .indexOf(5));
assert.equal(-1, .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', )
.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}
* @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', ).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', ).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;
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;
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;
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;
  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
页: [1]
查看完整版本: 带你入门带你飞Ⅰ 使用Mocha + Chai + Sinon单元测试Node.js