带你入门带你飞Ⅰ 使用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 如下
 var jdbc = require('../../lib/jdbc.js');
 var widgets = require('../../models/widgets.js');
 var chai = require('chai');
 var should = chai.should();
 var assert = chai.assert;
 var chaiAsPromised = require('chai-as-promised');
 chai.use(chaiAsPromised);
 var sinon = require('sinon');
 var Q = require('q');
 describe('Widgets', function() {
     describe('get widgets', function() {
         var stub;
         function jdbcPromise() {
             return Q.fcall(function() {
                 return [{
                     widgetId: 10
                 }];
             });
         };
         beforeEach(function() {
             stub = sinon.stub(jdbc, "query");
             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());
         });
         it('get widgets - 1', function() {
             return widgets.getWidgets(1).should.eventually.be.an('array');
         });
         afterEach(function() {
             stub.restore();
         });
     });
 });
被测代码返回的是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
     describe('get widgets - error', function() {
         var stub;
         function jdbcPromise() {
             return Q.fcall(function() {
                 throw new Error("widgets error");
             });
         };
         beforeEach(function() {
             stub= sinon.stub(jdbc, "query");
             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());
         });
         it('get widgets - error', function() {
             return widgets.getWidgets(1).should.be.rejectedWith('widgets error');
         });
         afterEach(function() {
             stub.restore();
         });
     });
运行测试 结果如下:

Example 2
接下来我想测试controller层, 那stub的对象就变成了widgets这个依赖了,
在这里我们用到了supertest来模拟发送http request, 类似功能的模块还有chai-http
如果我们不去stub,mock 的话也可以,这样利用supertest 来发送http request 测试controller->model->lib, 每层都测到了, 这就是Integration testing了。
 var kraken = require('kraken-js');
 var express = require('express');
 var request = require('supertest');
 var chai = require('chai');
 var assert = chai.assert;
 var sinon = require('sinon');
 var Q = require('q');
 var widgets = require('../../../../models/widgets.js');
 describe('/dashboard/widgets', function() {
   var app, mock;
   before(function(done) {
     app = express();
     app.on('start', done);
     app.use(kraken({
       basedir: process.cwd(),
       onconfig: function(config, next) {
         //some config info, such as login user info in req
     } }));
     mock = app.listen(1337);
   });
   after(function(done) {
     mock.close(done);
   });
   describe('get widgets', function() {
     var stub;
     function jdbcPromise() {
       return Q.fcall(function() {
         return {
           widgetId: 10
         };
       });
     };
     beforeEach(function() {
       stub = sinon.stub(widgets, "getWidgets");
       stub.withArgs('wade-xu').returns(jdbcPromise());
     });
     it('get widgets', function(done) {
       request(mock)
         .get('/dashboard/widgets/')
         .expect(200)
         .expect('Content-Type', /json/)
         .end(function(err, res) {
           if (err) return done(err);
           assert.equal(res.body.widgetId, '10');
           done();
         });
     });
     afterEach(function() {
       stub.restore();
     });
   });
 });
注意,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的行为, 代码如下:
 var mysql = require('mysql');
 var databaseConfig = require('../../config/config.json').database;
 var chai = require('chai');
 var assert = chai.assert;
 var expect = chai.expect;
 var should = chai.should();
 var sinon = require('sinon');
 var Q = require('q');
 var chaiAsPromised = require('chai-as-promised');
 chai.use(chaiAsPromised);
 var config = {
   connectionLimit: databaseConfig.connectionLimit,
   host: databaseConfig.host,
   user: databaseConfig.user,
   password: databaseConfig.password,
   port: databaseConfig.port,
   database: databaseConfig.database
 };
 describe('jdbc', function() {
   describe('mock query', function() {
     var stub;
     var spy;
     var myPool = {
       getConnection: function(cb) {
         var connection = {
           release: function() {},
           query: function(query, params, qcb) {
             var mockQueries = {
               q1: 'select * from t_widget where userId =?'
             }
             if (query === mockQueries.q1 && params === '81EFF5C2') {
               return qcb(null, 'success query');
             } else {
               return qcb(new Error('fail to query'));
             }
           }
         };
         spy = sinon.spy(connection, "release");
         cb(null, connection);
       }
     };
     beforeEach(function() {
       stub = sinon.stub(mysql, "createPool");
       stub.withArgs(config).returns(myPool);
     });
     it('query success', function() {
       delete require.cache[require.resolve('../../lib/jdbc.js')];
       var jdbc = require('../../lib/jdbc.js');
       jdbc.query('select * from t_widget where userId =?', '81EFF5C2').should.eventually.deep.equal('success query');
       assert(spy.calledOnce);
     });
     it('query error', function() {
       delete require.cache[require.resolve('../../lib/jdbc.js')];
       var jdbc = require('../../lib/jdbc.js');
       jdbc.query('select * from t_widget where userId =?', 'WrongID').should.be.rejectedWith('fail to query');
       assert(spy.calledOnce);
     });
     afterEach(function() {
       stub.restore();
       spy.restore();
     });
   });
   describe('mock query error ', function() {
     var stub;
     var spy;
     var myPool = {
       getConnection: function(cb) {
         var connection = {
           release: function() {},
         };
         spy = sinon.spy(connection, "release");
         cb(new Error('Pool get connection error'));
       }
     };
     beforeEach(function() {
       stub = sinon.stub(mysql, "createPool");
       stub.withArgs(config).returns(myPool);
     });
     it('query error without connection', function() {
       delete require.cache[require.resolve('../../lib/jdbc.js')];
       var jdbc = require('../../lib/jdbc.js');
       jdbc.query('select * from t_widget where userId =?', '81EFF5C2').should.be.rejectedWith('Pool get connection error');
       assert.isFalse(spy.called);
     });
     afterEach(function() {
       stub.restore();
       spy.restore();
     });
   });
 });
这里要注意的是我每个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
带你入门带你飞Ⅰ 使用Mocha + Chai + Sinon单元测试Node.js的更多相关文章
- 带你入门带你飞Ⅱ 使用Mocha + Chai + SuperTest测试Restful API in node.js
		目录 1. 简介 2. 准备开始 3. Restful API测试实战 Example 1 - GET Example 2 - Post Example 3 - Put Example 4 - Del ... 
- 大前端的自动化工厂(5)—— 基于Karma+Mocha+Chai的单元测试和接口测试
		一. 前端自动化测试 大多数前端开发者对测试相关的知识是比较缺乏的,一来是开发节奏很快,来不及写,另一方面团队里也配备了"人肉测试机",完全没必要自己来.但随着项目体量的增大,许多 ... 
- Node.js、express、mongodb 入门(基于easyui datagrid增删改查)
		前言 从在本机(win8.1)环境安装相关环境到做完这个demo大概不到两周时间,刚开始只是在本机安装环境并没有敲个Demo,从周末开始断断续续的想写一个,按照惯性思维就写一个增删改查吧,一方面是体验 ... 
- DTSE Tech Talk | 第10期:云会议带你入门音视频世界
		摘要:本期直播主题是<云会议带你入门音视频世界>,华为云媒体服务产品部资深专家金云飞,与开发者们交流华为云会议在实时音视频行业中的集成应用,帮助开发者更好的理解华为云会议及其开放能力. 本 ... 
- 可能是史上最强大的js图表库——ECharts带你入门
		PS:之前的那篇博客Highcharts——让你的网页上图表画的飞起 ,评论中,花儿笑弯了腰 和 StanZhai 两位仁兄让我试试 ECharts ,去主页看到<Why ECharts ?&g ... 
- 史上最强大的js图表库——ECharts带你入门(转)
		出处:http://www.cnblogs.com/zrtqsk/p/4019412.html PS:之前的那篇博客Highcharts——让你的网页上图表画的飞起 ,评论中,花儿笑弯了腰 和 Sta ... 
- C#单元测试,带你入门
		注:本文示例环境 VS2017 XUnit 2.2.0 单元测试框架 xunit.runner.visualstudio 2.2.0 测试运行工具 Moq 4.7.10 模拟框架 为什么要编写单元测试 ... 
- SQLite 带你入门
		SQLite数据库相较于我们常用的Mysql,Oracle而言,实在是轻量得不行(最低只占几百K的内存).平时开发或生产环境中使用各种类型的数据库,可能都需要先安装数据库服务(server),然后才能 ... 
- 一天带你入门到放弃vue.js(三)
		自定义指令 在上面学习了自定义组件接下来看一下自定义指令 自己新建的标签赋予特殊功能的是组件,而指定是在标签上使用类似于属性,以v-name开头,v-on,v-if...是系统指令! v-是表示这是v ... 
随机推荐
- C++类和对象
			1.在类体中和类体外定义成员函数是有区别的:在类体中定义的成员函数为内联(inline)函数,在类体外定义的不是.内联函数一般不是我们所期望的,它会将函数调用处用函数体替代,所以我建议在类体内部对成员 ... 
- 分布式事务(一)两阶段提交及JTA
			原创文章,同步发自作者个人博客 http://www.jasongj.com/big_data/two_phase_commit/ 分布式事务 分布式事务简介 分布式事务是指会涉及到操作多个数据库(或 ... 
- python内建函数
			人太懒了,博客就慢慢添加吧,这个话题还有很多要学的,后面实践了再来添加,现在就当是开个头. print(dir(__builtins__)) #获取内建属性.函数列表 print(help( ... 
- 2015.10.18 do while练习
			/*乘法表*/ #define COLMAX 10 #define ROWMAX 12 main() { int row,column,y; row=1; printf(" ... 
- 写一些封装part1 (事件绑定移除,圆形矩形碰撞检测)
			var EventHandle = { addEvent:function(ele,type,handle){ if (ele.addEventListener) { ele.addEventList ... 
- SHOI 2009 会场预约 平衡树 STL练习
			题目描述 PP大厦有一间空的礼堂,可以为企业或者单位提供会议场地.这些会议中的大多数都需要连续几天的时间(个别的可能只需要一天),不过场地只有一个,所以不同的会议的时间申请不能够冲突.也就是说,前一个 ... 
- Codeforces Round #379 (Div. 2) E. Anton and Tree
			题意: 给一颗树 每个节点有黑白2色 可以使一个色块同事变色,问最少的变色次数. 思路: 先缩点 把一样颜色的相邻点 缩成一个 然后新的树 刚好每一层是一个颜色. 最后的答案就是树的直径/2 不过我用 ... 
- POJ 3666 Making the Grade
			Description A straight dirt road connects two fields on FJ's farm, but it changes elevation more tha ... 
- ext.net与extjs的关系
			一.在客户端,ext.net中可以使用extjs的语法. 如发送ajax请求, Ext.Ajax.request({ url: 'Common/getNode', ... 
- asmlib
			http://pandarabbit.blog.163.com/blog/static/209284144201292293642857/ centos6.5桌面2.6.32yum install k ... 
