带你入门带你飞Ⅰ 使用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

时间: 2024-10-27 12:58:18

带你入门带你飞Ⅰ 使用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 - Delete 4. Troubleshooting 5. 参考文档 简介 经过上一篇文章的介绍,相信你已经对mocha, chai有一定的了解了, 本篇主要讲述如何用supertest来测试nodejs项目中的Restful API, 项目基于express框架. SuperTest 是 SuperAge

大前端的自动化工厂(5)—— 基于Karma+Mocha+Chai的单元测试和接口测试

一. 前端自动化测试 大多数前端开发者对测试相关的知识是比较缺乏的,一来是开发节奏很快,来不及写,另一方面团队里也配备了"人肉测试机",完全没必要自己来.但随着项目体量的增大,许多人维护同一份代码,经常会出现有些函数莫名其妙地结果不对了,或者某个接口的入参变了,又或者哪位大哥把后端返回的数据结构给改了.每天工作的时间里被拉来拉去帮人定位问题,结果花了很多时间却发现大部分都是别人的锅.每当遇到项目上线,那就更热闹了,跟着其他"人肉测试机"大家一起点点点...... 很

Node.js、express、mongodb 入门(基于easyui datagrid增删改查)

前言 从在本机(win8.1)环境安装相关环境到做完这个demo大概不到两周时间,刚开始只是在本机安装环境并没有敲个Demo,从周末开始断断续续的想写一个,按照惯性思维就写一个增删改查吧,一方面是体验下node.js的魔力,二就是看看node.js.express和mongoose的API,其次就是把自己入门的过程记录下来,方便自己查看,再就是对入门的朋友起一个引导的作用. 敲demo的过程中感觉最爽的就是npm(Node Package Manager)是一个Node.js的包管理和分发工具.

五一装修优惠那么大,云麦装修带你装修带你飞

"五一"小长假来临之际,看着别人都在做着出行计划,你还在埋头苦查装修攻略吗?想着别人又要晒美食晒美景,你还在计划到材料城跑断腿吗?不如,把房子装修交给我们,你免费旅游去! 云麦装饰-米图原创设计机构大明宫钻石店更推出"五一放松计划",活动期间签订装修合同客户,即可参加抽奖,旅游基金百分百中奖!韩国首尔五天四日双人游,追逐太阳的后裔宋欧巴的脚步!香港迪士尼,珠海长隆海洋王国双人套餐!更有丽江/成都/三亚/厦门/贵阳等往返机票!300/500携程旅游基金,你想去哪就去哪

GDI+入门——带你走进Windows图形的世界

一.GDI+基础 1.GDI+简介 GDI+是微软的新一代二维图形系统,它完全面向对象,要在Windows窗体中显示字体或绘制图形必须要使用GDI+.GDI+提供了多种画笔.画刷.图像等图形对象,此外还包括一些新的绘图功能,比如渐变.除锯齿.纹理等. GDI+包括三部分:二维矢量图形的绘制.图像处理和文字显示.GDI+使用的各种类大都包含在命名空间system::Drawing中. 2.常用的数据结构 在使用GDI+显示文字和绘制图形时,需要用到一些数据结构,例如Size.Point.Recta

可能是史上最强大的js图表库——ECharts带你入门

PS:之前的那篇博客Highcharts——让你的网页上图表画的飞起 ,评论中,花儿笑弯了腰 和 StanZhai 两位仁兄让我试试 ECharts ,去主页看到<Why ECharts ?>简单了解了一下之后,ECharts很快吸引了我.里面引自马云的那句话“互联网还没有搞清楚的时候,移动互联网来了,移动互联没有搞清楚的时候,大数据来了”我是第一次听到,实在震撼了我啊(孤陋寡闻...). 本来没打算写什么的.可是作为一个后端开发者,看了半天文档也迷迷糊糊,查了一堆资料也没搞懂Echarts那

Node.js 新手入门招集,免费!带10个项目~ 2015 带你一起飞 ~

高手可以ctrl+w了 好吧,其实我只是一个写JS的. 当年node.js发布,我感觉没有我的份.最后还是要去了解一下,虽然好像似乎它有许多不尽人意的地方.但是,我感觉它撑下来了,撑住了舆论,撑住了各种对比,我希望留下的是better things.我不是高手,我也是一个在学习node.js的学习者.一个不新不旧的人,做过一个node.js的项目,但是它烂了.最后又重新开始,一边学习,一边做项目. 我自己打算做的一个练手项目 https://github.com/coolicer/shopsho

我在爱板网写的-- 【望月追忆】带你入门STM32F0系列文章

[望月追忆]带你入门STM32F0之前传:STM32F0资料 [望月追忆]带你入门STM32F0之环境搭建 [望月追忆]带你入门STM32F0之一:STM32F0概述 [望月追忆]带你入门STM32F0之二:SysTick时钟介绍 [望月追忆]带你入门STM32F0之二:点亮你的小灯 [望月追忆]带你入门STM32F0之三:按键----查询方式 [望月追忆]带你入门STM32F0之四:按键----外部中断 [望月追忆]带你入门STM32F0之四:串口 [望月追忆]带你入门STM32F0之五:小项

史上最强大的js图表库——ECharts带你入门(转)

出处:http://www.cnblogs.com/zrtqsk/p/4019412.html PS:之前的那篇博客Highcharts——让你的网页上图表画的飞起 ,评论中,花儿笑弯了腰 和 StanZhai 两位仁兄让我试试 ECharts ,去主页看到<Why ECharts ?>简单了解了一下之后,ECharts很快吸引了我.里面引自马云的那句话“互联网还没有搞清楚的时候,移动互联网来了,移动互联没有搞清楚的时候,大数据来了”我是第一次听到,实在震撼了我啊(孤陋寡闻...). 本来没打