目錄
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