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

  • 相关阅读:
    第1次作业
    第0次作业
    总结报告
    第14、15周作业
    第七周作业
    第六周作业
    第四周作业
    第四次作业
    第三次作业
    2018第二次作业
  • 原文地址:https://www.cnblogs.com/wade-xu/p/4665250.html
Copyright © 2020-2023  润新知