1. 目录
  2. 1. 简介
  3. 2. 前提条件
  4. 3. Mocha入门
  5. 4. Mocha实战
  6. 被测代码
  7.   Example 1
  8.   Example 2
  9.   Example 3
  10. 5. Troubleshooting
  11. 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 后执行如下命令:

  1. npm install -g mocha
  1. npm install sinon
  1. npm install chai

## mocha global 安装是为了能够在命令行下面使用命令。

Mocha入门

以下为最简单的一个mocha示例:

  1. var assert = require("assert");
  2. describe('Array', function(){
  3. describe('#indexOf()', function(){
  4. it('should return -1 when the value is not present', function(){
  5. assert.equal(-1, [1,2,3].indexOf(5));
  6. assert.equal(-1, [1,2,3].indexOf(0));
  7. })
  8. })
  9. });
  • 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

文件目录结构如下

  1. ├── config
  2. └── config.json
  3. ├── controllers
  4. └── dashboard
  5. └── widgets
  6. └── index.js
  7. ├── models
  8. └── widgets.js
  9. ├── lib
  10. └── jdbc.js
  11. ├── package.json
  12. └── test
  13. ├── controllers
  14. └── dashboard
  15. └── widgets
  16. └── index_MockTest.js
  17. ├── models
  18. └── widgetsTest.js
  19. └── lib
  20. └── jdbc_mockTest.js

##转载注明出处:http://www.cnblogs.com/wade-xu/p/4665250.html

被测代码

Controller/dashboard/widgets/index.js

  1. var _widgets = require('../../../models/widgets.js');
  2.  
  3. module.exports = function(router) {
  4.  
  5. router.get('/', function(req, res) {
  6. _widgets.getWidgets(req.user.id)
  7. .then(function(widgets){
  8. return res.json(widgets);
  9. })
  10. .catch(function(err){
  11. return res.json ({
  12. code: '000-0001',
  13. message: 'failed to get widgets:'+err
  14. });
  15. });
  16. });
  17. };

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

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

  4. /**
  5. * Get user widgets
  6. * @param {String} userId
  7. * @return {Promise}
  8. */
  9. Widgets.getWidgets = function(userId) {
  10. var defer = Q.defer();
  11. 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])
  12. .then(function(rows){
  13. defer.resolve(convertRows(rows));
  14. }).catch(function(err){
  15. defer.reject(err);
  16. });
  17.  
  18. return defer.promise;
  19. };

lib/jdbc.js  -- function 连接数据库查询

  1. var mysql = require('mysql');
  2. var Promise = require('q');
  3. var databaseConfig = require('../config/config.json').database;
  4.  
  5. var JDBC_MYSQL = exports;
  6.  
  7. var pool = mysql.createPool({
  8. connectionLimit: databaseConfig.connectionLimit,
  9. host: databaseConfig.host,
  10. user: databaseConfig.user,
  11. password: databaseConfig.password,
  12. port: databaseConfig.port,
  13. database: databaseConfig.database
  14. });
  15.  
  16. /**
  17. * Run database query
  18. * @param {String} query
  19. * @param {Object} [params]
  20. * @return {Promise}
  21. */
  22. JDBC_MYSQL.query = function(query, params) {
  23. var defer = Promise.defer();
  24. params = params || {};
  25. pool.getConnection(function(err, connection) {
  26. if (err) {
  27. if (connection) {
  28. connection.release();
  29. }
  30. return defer.reject(err);
  31. }
  32. connection.query(query, params, function(err, results){
  33. if (err) {
  34. if (connection) {
  35. connection.release();
  36. }
  37. return defer.reject(err);
  38. }
  39. connection.release();
  40. defer.resolve(results);
  41. });
  42. });
  43. return defer.promise;
  44. };

config/config.json   --数据库配置

  1. {
  2. "database": {
  3. "host" : "10.46.10.007",
  4. "port" : 3306,
  5. "user" : "wadexu",
  6. "password" : "wade001",
  7. "database" : "demo",
  8. "connectionLimit" : 100
  9. }
  10. }

##转载注明出处: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. describe('get widgets', function() {
  17.  
  18. var stub;
  19.  
  20. function jdbcPromise() {
  21. return Q.fcall(function() {
  22. return [{
  23. widgetId: 10
  24. }];
  25. });
  26. };
  27.  
  28. beforeEach(function() {
  29. stub = sinon.stub(jdbc, "query");
  30. 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());
  31.  
  32. });
  33.  
  34. it('get widgets - 1', function() {
  35. return widgets.getWidgets(1).should.eventually.be.an('array');
  36. });
  37.  
  38. afterEach(function() {
  39. stub.restore();
  40. });
  41. });
  42. });

被测代码返回的是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. beforeEach(function() {
  52. stub = sinon.stub(mysql, "createPool");
  53. stub.withArgs(config).returns(myPool);
  54.  
  55. });
  56.  
  57. it('query success', function() {
  58. delete require.cache[require.resolve('../../lib/jdbc.js')];
  59. var jdbc = require('../../lib/jdbc.js');
  60. jdbc.query('select * from t_widget where userId =?', '81EFF5C2').should.eventually.deep.equal('success query');
  61. assert(spy.calledOnce);
  62. });
  63.  
  64. it('query error', function() {
  65. delete require.cache[require.resolve('../../lib/jdbc.js')];
  66. var jdbc = require('../../lib/jdbc.js');
  67. jdbc.query('select * from t_widget where userId =?', 'WrongID').should.be.rejectedWith('fail to query');
  68. assert(spy.calledOnce);
  69. });
  70.  
  71. afterEach(function() {
  72. stub.restore();
  73. spy.restore();
  74. });
  75.  
  76. });
  77.  
  78. describe('mock query error ', function() {
  79.  
  80. var stub;
  81. var spy;
  82.  
  83. var myPool = {
  84. getConnection: function(cb) {
  85. var connection = {
  86. release: function() {},
  87. };
  88. spy = sinon.spy(connection, "release");
  89. cb(new Error('Pool get connection error'));
  90. }
  91. };
  92.  
  93. beforeEach(function() {
  94. stub = sinon.stub(mysql, "createPool");
  95. stub.withArgs(config).returns(myPool);
  96. });
  97.  
  98. it('query error without connection', function() {
  99. delete require.cache[require.resolve('../../lib/jdbc.js')];
  100. var jdbc = require('../../lib/jdbc.js');
  101. jdbc.query('select * from t_widget where userId =?', '81EFF5C2').should.be.rejectedWith('Pool get connection error');
  102.  
  103. assert.isFalse(spy.called);
  104. });
  105.  
  106. afterEach(function() {
  107. stub.restore();
  108. spy.restore();
  109. });
  110.  
  111. });
  112.  
  113. });

这里要注意的是我每个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的更多相关文章

  1. 带你入门带你飞Ⅱ 使用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 ...

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

    一. 前端自动化测试 大多数前端开发者对测试相关的知识是比较缺乏的,一来是开发节奏很快,来不及写,另一方面团队里也配备了"人肉测试机",完全没必要自己来.但随着项目体量的增大,许多 ...

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

    前言 从在本机(win8.1)环境安装相关环境到做完这个demo大概不到两周时间,刚开始只是在本机安装环境并没有敲个Demo,从周末开始断断续续的想写一个,按照惯性思维就写一个增删改查吧,一方面是体验 ...

  4. DTSE Tech Talk | 第10期:云会议带你入门音视频世界

    摘要:本期直播主题是<云会议带你入门音视频世界>,华为云媒体服务产品部资深专家金云飞,与开发者们交流华为云会议在实时音视频行业中的集成应用,帮助开发者更好的理解华为云会议及其开放能力. 本 ...

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

    PS:之前的那篇博客Highcharts——让你的网页上图表画的飞起 ,评论中,花儿笑弯了腰 和 StanZhai 两位仁兄让我试试 ECharts ,去主页看到<Why ECharts ?&g ...

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

    出处:http://www.cnblogs.com/zrtqsk/p/4019412.html PS:之前的那篇博客Highcharts——让你的网页上图表画的飞起 ,评论中,花儿笑弯了腰 和 Sta ...

  7. C#单元测试,带你入门

    注:本文示例环境 VS2017 XUnit 2.2.0 单元测试框架 xunit.runner.visualstudio 2.2.0 测试运行工具 Moq 4.7.10 模拟框架 为什么要编写单元测试 ...

  8. SQLite 带你入门

    SQLite数据库相较于我们常用的Mysql,Oracle而言,实在是轻量得不行(最低只占几百K的内存).平时开发或生产环境中使用各种类型的数据库,可能都需要先安装数据库服务(server),然后才能 ...

  9. 一天带你入门到放弃vue.js(三)

    自定义指令 在上面学习了自定义组件接下来看一下自定义指令 自己新建的标签赋予特殊功能的是组件,而指定是在标签上使用类似于属性,以v-name开头,v-on,v-if...是系统指令! v-是表示这是v ...

随机推荐

  1. Lua table库整理(v5.1)

    这个库提供了表处理的通用函数. 所有函数都放在表 table. 无论何时,若一个操作需要取表的长度, 这张表必须是一个真序列. table.concat(list, [, sep, [, i , [, ...

  2. C# Lock 解读 (关键是理解最后一句)

    最近在研究.NET分布式缓存代码,正好涉及Lock,看了网上的文章,总结了一些Lock相关的知识,供大家一起学习参考. 一.Lock定义     lock 关键字可以用来确保代码块完成运行,而不会被其 ...

  3. 第七章 LED将为我们闪烁:控制发光二极管

     第七章 LED将为我们闪烁:控制发光二极管 本章我们将会看到一个完整的linux驱动程序,通过linux驱动程序控制LED的四个小灯,通俗的说就是通过向linux驱动程序来控制LED小灯的开关.用到 ...

  4. java程序员烂大街为何还不便宜?

    最近跟一朋友聊天,他是做c#开发的.他答应了老板带领一帮java工程师开发网站.披星戴月终于搞定,现在已经盈利.但是他公司的那帮搞c#的同事不淡定了. 在招聘java程序员的时候2年有开15k的.5年 ...

  5. RY哥查字典

    时间限制: 1 s 空间限制: 16000 KB 题目等级 : 钻石 Diamond 题目描述 Description RY哥最近新买了一本字典,他十分高兴,因为这上面的单词都十分的和谐,他天天查字典 ...

  6. 用shebang编写一个ssh自动登陆脚本

    单例模式是软件开发中非常普遍的一种模式.它的主要作用是确保系统中,始终只存在一个类的实例对象. 这样做的好处有两点: 1.对于需要频繁使用的对象,在每次使用时,如果都需要重新创建,并且这些对象的内容都 ...

  7. System.DateUtils 2. IsInLeapYear 判断是否是闰年

    编译版本:Delphi XE7 function IsInLeapYear(const AValue: TDateTime): Boolean; implementation // 判断是否是闰年 f ...

  8. tomcat building

    https://tomcat.apache.org/tomcat-7.0-doc/building.html https://tomcat.apache.org/tomcat-7.0-doc/BUIL ...

  9. 线程安全及Python中的GIL

    线程安全及Python中的GIL 本博客所有内容采用 Creative Commons Licenses 许可使用. 引用本内容时,请保留 朱涛, 出处 ,并且 非商业 . 点击 订阅 来订阅本博客. ...

  10. VB CreateObject转C#

    C#调用方法.函数获取属性大致流程如下: System.Type oType = System.Type.GetTypeFromProgID("SomeClass"); objec ...