├── .gitignore ├── .travis.yml ├── Makefile ├── README.md ├── index.js ├── lib └── index.js ├── node_unit_testing.md ├── package.json └── test └── index.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | coverage.html 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | - 0.10 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.test.js 2 | REPORTER = spec 3 | TIMEOUT = 10000 4 | JSCOVERAGE = ./node_modules/jscover/bin/jscover 5 | 6 | test: 7 | @NODE_ENV=test ./node_modules/mocha/bin/mocha -R $(REPORTER) -t $(TIMEOUT) $(TESTS) 8 | 9 | test-cov: lib-cov 10 | @LIB_COV=1 $(MAKE) test REPORTER=dot 11 | @LIB_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html 12 | 13 | lib-cov: 14 | @rm -rf ./lib-cov 15 | @$(JSCOVERAGE) lib lib-cov 16 | 17 | .PHONY: test test-cov lib-cov 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 单元测试示例项目 2 | ======= 3 | ## Slides 4 | NodeParty分享于网易杭研。markdown:[Node unit testing](https://github.com/JacksonTian/unittesting/blob/master/node_unit_testing.md) slides: [Node unit testing](http://html5ify.com/unittesting/slides/index.html) 5 | 6 | ## 项目状态 7 | 单元测试状态: [![ci](https://api.travis-ci.org/JacksonTian/unittesting.png?branch=master)](http://travis-ci.org/JacksonTian/unittesting) 8 | 单元测试覆盖率:[100%](http://html5ify.com/unittesting/coverage.html) 9 | 10 | ## 项目链接 11 | - mocha/should 12 | - rewire/muk/pendding -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = process.env.LIB_COV ? require('./lib-cov/index') : require('./lib/index'); 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | exports.limit = function (num) { 4 | if (num < 0) { 5 | return 0; 6 | } 7 | return num; 8 | }; 9 | 10 | 11 | var limit = function (num) { 12 | return num < 0 ? 0 : num; 13 | }; 14 | 15 | exports.limit2 = function (num) { 16 | return limit(num); 17 | }; 18 | 19 | exports.async = function (callback) { 20 | setTimeout(function () { 21 | callback(10); 22 | }, 10); 23 | }; 24 | 25 | exports.asyncTimeout = function (callback) { 26 | setTimeout(function () { 27 | callback(10); 28 | }, 6000); 29 | }; 30 | 31 | exports.parseAsync = function (input, callback) { 32 | setTimeout(function () { 33 | var result; 34 | try { 35 | result = JSON.parse(input); 36 | } catch (e) { 37 | return callback(e); 38 | } 39 | callback(null, result); 40 | }, 10); 41 | }; 42 | 43 | exports.getContent = function (filename, callback) { 44 | fs.readFile(filename, 'utf-8', callback); 45 | }; 46 | -------------------------------------------------------------------------------- /node_unit_testing.md: -------------------------------------------------------------------------------- 1 | Node.js Unit Testing 2 | ========= 3 | ### by @朴灵 4 | 5 | ## Agenda 6 | - 什么是单元测试? 7 | - 测试框架 8 | - 断言库 9 | - 测试用例 10 | - 覆盖率 11 | - Mock 12 | - 私有方法测试 13 | - 持续集成 14 | 15 | ## 讲什么不讲什么 16 | - 讲单元测试过程中的关键点和关键流程 17 | - 不讲单元测试过程中的过小的细节 18 | 19 | ## 什么是单元测试 20 | `demo.js` by @代码诗人芋头 21 | 22 | ``` 23 | var dp = require("../lib/index.js"); 24 | dp.process(process.cwd().replace(/demo$/,""), { 25 | blackList:[ 26 | // "*demo.js" 27 | ] 28 | }, function (error,result) { 29 | if(error){ 30 | //code=1 是致命错误,code=2 是请求错误,不致命。 31 | }else{ 32 | console.log(result); 33 | } 34 | }); 35 | ``` 36 | ## 单元测试的认知误区 37 | - 单元测试是QA妹纸的事情 38 | - 示例代码不是单元测试? 39 | - 自己测试自己的代码意义何在? 40 | - 版本更新迭代维护单元测试的成本很高? 41 | - 我这么牛逼,我就是不写单元测试,你咬我丫 42 | 43 | ## 今天之任务 44 | 编写一个稳定可靠的模块 45 | 46 | 1. 模块具备limit方法,输入一个数值,小于0的时候返回0,其余正常返回 47 | 48 | ``` 49 | exports.limit = function (num) { 50 | if (num < 0) { 51 | return 0; 52 | } 53 | return num; 54 | }; 55 | ``` 56 | 57 | ## 目录分配 58 | 59 | - `lib`,存放模块代码的地方 60 | - `test`,存放单元测试代码的地方 61 | - `index.js`,向外导出模块的地方 62 | - `package.json`,包描述文件 63 | 64 | ## 测试框架 65 | 测试框架 66 | 67 | - Mocha。`npm install mocha -g` 68 | 69 | 开发依赖/devDependencies 70 | 71 | "devDependencies": { 72 | "mocha": "*" 73 | } 74 | 75 | ## 测试接口 76 | - BDD/行为驱动开发 77 | - TDD/测试驱动开发 78 | 79 | 我们选择BDD,更贴近于思考方式 80 | 81 | ## BDD 82 | - `describe()` 83 | - `it()` 84 | 85 | ``` 86 | describe('module', function () { 87 | describe('limit', function () { 88 | it('limit should success', function () { 89 | lib.limit(10); 90 | }); 91 | }); 92 | }); 93 | ``` 94 | 95 | ## BDD结果 96 | 在当前目录下执行`mocha`: 97 | 98 | ``` 99 | test_lib jacksontian $ mocha 100 | 101 | ․ 102 | 103 | ✔ 1 test complete (2ms) 104 | 105 | 106 | ``` 107 | 108 | ## BDD Hook 109 | - `before()` 110 | - `after()` 111 | 112 | ``` 113 | describe('module', function () { 114 | before(function () { 115 | console.log('Pre something'); 116 | }); 117 | describe('limit', function () { 118 | it('limit should success', function () { 119 | lib.limit(10); 120 | }); 121 | }); 122 | after(function () { 123 | console.log('Post something'); 124 | }); 125 | }); 126 | ``` 127 | ## BDD Hook(2) 128 | - `beforeEach()` 129 | - `afterEach()` 130 | 131 | ``` 132 | describe('module', function () { 133 | beforeEach(function () { 134 | console.log('Pre something'); 135 | }); 136 | describe('limit', function () { 137 | it('limit should success', function () { 138 | lib.limit(10); 139 | }); 140 | }); 141 | afterEach(function () { 142 | console.log('Post something'); 143 | }); 144 | }); 145 | ``` 146 | ## TDD 147 | - `suite` 148 | - `test` 149 | 150 | ``` 151 | suite('module', function() { 152 | suite('limit', function() { 153 | test('limit should success', function () { 154 | lib.limit(10); 155 | }); 156 | }); 157 | }); 158 | ``` 159 | 160 | ## TDD Hook 161 | - `setup` 162 | - `teardown` 163 | 164 | ``` 165 | suite('module', function() { 166 | setup(function () { 167 | console.log('Pre something'); 168 | }); 169 | suite('limit', function() { 170 | test('limit should success', function () { 171 | lib.limit(10); 172 | }); 173 | }); 174 | teardown(function () { 175 | console.log('Post something'); 176 | }); 177 | }); 178 | ``` 179 | 180 | ## 断言库 181 | 等等!我们还没检查结果呢,这算什么鸟测试呢,测试你妹啊。 182 | 183 | 断言库: 184 | 185 | - should.js 186 | - expect.js 187 | - chai 188 | 189 | ## 加上断言 190 | 191 | ``` 192 | test('limit should success', function () { 193 | lib.limit(10).should.be.equal(10); 194 | }); 195 | ``` 196 | 197 | 198 | ## 结果输出 199 | 200 | ``` 201 | test_lib jacksontian $ mocha --reporters 202 | 203 | dot - dot matrix 204 | doc - html documentation 205 | spec - hierarchical spec list 206 | json - single json object 207 | progress - progress bar 208 | list - spec-style listing 209 | tap - test-anything-protocol 210 | landing - unicode landing strip 211 | xunit - xunit reportert 212 | teamcity - teamcity ci support 213 | html-cov - HTML test coverage 214 | json-cov - JSON test coverage 215 | min - minimal reporter (great with --watch) 216 | json-stream - newline delimited json events 217 | markdown - markdown documentation (github flavour) 218 | nyan - nyan cat! 219 | ``` 220 | 221 | ``` 222 | mocha -R spec 223 | mocha -R nyan 224 | ``` 225 | ## 测试用例 226 | 需求变更啦: 227 | `limit`这个方法还要求返回值大于100时返回100。 228 | 229 | 正向测试/反向测试 230 | 231 | ## 重构代码 232 | ``` 233 | exports.limit = function (num) { 234 | return num < 0 ? 0 : num; 235 | }; 236 | ``` 237 | 238 | ## 测试用例的价值 239 | 问题? 240 | 241 | - 如何确保你的改动对原有成果没有造成破坏? 242 | - 如何验证本次的需求是被满足的? 243 | 244 | ## 异步怎么测试? 245 | 如何测试? 246 | 247 | ``` 248 | exports.async = function (callback) { 249 | setTimeout(function () { 250 | callback(10); 251 | }, 10); 252 | }; 253 | ``` 254 | 255 | ## 测试异步代码 256 | 257 | ``` 258 | describe('async', function () { 259 | it('async', function (done) { 260 | lib.async(function (result) { 261 | done(); 262 | }); 263 | }); 264 | }); 265 | ``` 266 | ## 异步方法的超时支持 267 | ``` 268 | exports.asyncTimeout = function (callback) { 269 | setTimeout(function () { 270 | callback(10); 271 | }, 6000); 272 | }; 273 | ``` 274 | 275 | ``` 276 | mocha -t 10000 277 | ``` 278 | 279 | ## 异步方法的异常处理 280 | 281 | ``` 282 | exports.parseAsync = function (input, callback) { 283 | setTimeout(function () { 284 | var result; 285 | try { 286 | result = JSON.parse(input); 287 | } catch (e) { 288 | return callback(e); 289 | } 290 | callback(null, result); 291 | }, 10); 292 | }; 293 | ``` 294 | 295 | ## 你的Case覆盖完全吗? 296 | 单元测试重要指标: 297 | 298 | - 覆盖率 299 | 300 | 模块: 301 | 302 | - `npm install jscover` 303 | 304 | ## 生成可被追踪的代码 305 | 306 | ``` 307 | ./node_modules/.bin/jscover lib lib-cov 308 | ``` 309 | 310 | ``` 311 | _$jscoverage['index.js'].source = ["exports.limit = function (input) {"," return input < 0 ? 0 : input;","};"]; 312 | _$jscoverage['index.js'][1]++; 313 | exports.limit = function(input) { 314 | _$jscoverage['index.js'][2]++; 315 | return input < 0 ? 0 : input; 316 | }; 317 | ``` 318 | 319 | ## 测试时引入追踪代码 320 | ``` 321 | module.exports = process.env.LIB_COV ? require('./lib-cov/index') : require('./lib/index'); 322 | ``` 323 | 备注,每个模块应该用自己的环境变量,以防止冲突 324 | 325 | ## 生成HTML覆盖率结果页 326 | 327 | ``` 328 | // 设置当前命令行有效的变量 329 | export LIB_COV=1 330 | mocha -R html-cov > coverage.html 331 | ``` 332 | 333 | ## Mock 334 | 异常该怎么测试? 335 | 336 | ``` 337 | exports.getContent = function (filename, callback) { 338 | fs.readFile(filename, 'utf-8', callback); 339 | }; 340 | ``` 341 | 342 | ## 简单mock 343 | hook派上用场了 344 | 345 | ``` 346 | describe("getContent", function () { 347 | var _readFile; 348 | before(function () { 349 | _readFile = fs.readFile; 350 | fs.readFile = function (filename, encoding, callback) { 351 | callback(new Error("mock readFile error")); 352 | }; 353 | }); 354 | // it(); 355 | after(function () { 356 | // 用完之后记得还原。否则影响其他case 357 | fs.readFile = _readFile; 358 | }) 359 | }); 360 | ``` 361 | 362 | ## 谨慎mock 363 | 异步接口依旧需要保持异步 364 | 365 | ``` 366 | fs.readFile = function (filename, encoding, callback) { 367 | process.nextTick(function () { 368 | callback(new Error("mock readFile error")); 369 | }); 370 | }; 371 | ``` 372 | 373 | ## Mock库 374 | Mock小模块:`muk` 375 | 376 | ``` 377 | var fs = require('fs'); 378 | var muk = require('muk'); 379 | 380 | muk(fs, 'readFile', function(path, callback) { 381 | process.nextTick(function () { 382 | callback(new Error("mock readFile error")); 383 | }); 384 | }); 385 | ``` 386 | 387 | ## 略微优美 388 | 389 | ``` 390 | before(function () { 391 | muk(fs, 'readFile', function(path, encoding, callback) { 392 | process.nextTick(function () { 393 | callback(new Error("mock readFile error")); 394 | }); 395 | }); 396 | }); 397 | // it(); 398 | after(function () { 399 | muk.restore(); 400 | }); 401 | ``` 402 | 403 | ## 测试私有方法 404 | 模块:[`rewire`](http://jhnns.github.com/rewire/) 405 | 406 | 今天老板说,limit方法不能再对外暴露了。如何测试它? 407 | 408 | ## 通过rewire导出方法 409 | ``` 410 | it('limit should return success', function () { 411 | var lib = rewire('../lib/index.js'); 412 | var litmit = lib.__get__('limit'); 413 | litmit(10); 414 | }); 415 | ``` 416 | 417 | ## rewire原理 418 | 【闭包原理】加载文件时注入`__set__`和`__get__`方法。该方法可以访问内部变量。 419 | 420 | ``` 421 | (function (exports, require, module, __filename, __dirname) { 422 | var method = function () {}; 423 | exports.__set__ = function (name, value) { 424 | eval(name " = " value.toString()); 425 | }; 426 | exports.__get__ = function (name) { 427 | return eval(name); 428 | }; 429 | }); 430 | ``` 431 | 432 | ## 用Makefile串起项目 433 | ``` 434 | TESTS = test/*.test.js 435 | REPORTER = spec 436 | TIMEOUT = 10000 437 | JSCOVERAGE = ./node_modules/jscover/bin/jscover 438 | 439 | test: 440 | @NODE_ENV=test ./node_modules/mocha/bin/mocha -R $(REPORTER) -t $(TIMEOUT) $(TESTS) 441 | 442 | test-cov: lib-cov 443 | @LIB_COV=1 $(MAKE) test REPORTER=dot 444 | @LIB_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html 445 | 446 | lib-cov: 447 | @rm -rf ./lib-cov 448 | @$(JSCOVERAGE) lib lib-cov 449 | 450 | .PHONY: test test-cov lib-cov 451 | ``` 452 | 453 | ``` 454 | make test 455 | make test-cov 456 | ``` 457 | 458 | 用项目自身的jscover和mocha,避免版本冲突和混乱 459 | 460 | ## 不持续集成不舒服 461 | - [Travis-ci](https://travis-ci.org/) 462 | - 绑定Github帐号 463 | - 在Github仓库的Admin打开Services hook 464 | - 打开Travis 465 | - 每次push将会hook触发执行`npm test`命令 466 | 467 | ## 不持续集成不舒服2 468 | 注意:Travis会将未描述的项目当作Ruby项目。所以需要在根目录下加入`.travis.yml`文件。内容如下: 469 | 470 | ``` 471 | language: node_js 472 | node_js: 473 | - 0.6 474 | - 0.8 475 | ``` 476 | 477 | ![](https://secure.travis-ci.org/JacksonTian/bagpipe.png) 478 | or ![](https://secure.travis-ci.org/TBEDP/datavjs.png) 479 | 480 | ## 总结 481 | - 使代码可以放心修改和重构 482 | - 食自己的狗食 483 | - 只有质量保证的代码才能有质量保证的产品 484 | - 写好代码和测试,把查找bug的时间用来干更有意义的事情 485 | - 单元测试Passing和覆盖率100%是一种荣耀 486 | - 有单元测试的代码,再差也不会差到哪里去 487 | - 没单元测试,吹牛逼也要小心 488 | - 集成的,好喝的 489 | 490 | ## TODO 491 | - 前后端共用单元测试 492 | - 断言的细节和技巧 493 | - Mocha的更多技巧 494 | - connect/express web应用的测试 495 | - `supertest` 496 | - 性能测试/功能测试 497 | 498 | ## QA && Thanks 499 | 500 | ## More 501 | - [单元测试示例项目](https://github.com/JacksonTian/unittesting) 502 | - [单元测试实战](fengmk2.github.com/ppt/unittest-and-bdd-in-nodejs-with-mocha.html) 内容与本PPT有互补 503 | - Mocha/Should/Chai/Except/Assert 504 | - rewire/pedding/supertest/muk -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unittesting", 3 | "version": "0.0.1", 4 | "description": "单元测试示例项目", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "make test" 11 | }, 12 | "devDependencies": { 13 | "mocha": "*", 14 | "should": "*", 15 | "muk": "*", 16 | "rewire": "*" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/JacksonTian/unittesting.git" 21 | }, 22 | "author": "Jackson Tian", 23 | "license": "MIT", 24 | "dependencies": { 25 | "jscover": "~0.2.1", 26 | "muk": "~0.2.0", 27 | "rewire": "~1.0.3", 28 | "should": "~1.2.1" 29 | }, 30 | "keywords": [ 31 | "unit", 32 | "testing", 33 | "mocha", 34 | "should" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | var lib = require('../'); 2 | var should = require('should'); 3 | var fs = require('fs'); 4 | var muk = require('muk'); 5 | var rewire = require('rewire'); 6 | 7 | describe('module', function () { 8 | describe('limit', function () { 9 | it('limit should success', function () { 10 | lib.limit(10).should.be.equal(10); 11 | }); 12 | 13 | it('limit should ok when less than 0', function () { 14 | lib.limit(-1).should.be.equal(0); 15 | }); 16 | }); 17 | 18 | describe('async', function () { 19 | it('async', function (done) { 20 | lib.async(function (result) { 21 | done(); 22 | }); 23 | }); 24 | }); 25 | 26 | describe('asyncTimeout', function () { 27 | it('async should ok', function (done) { 28 | lib.asyncTimeout(function (result) { 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('parseAsync', function () { 35 | it('parseAsync should ok', function (done) { 36 | lib.parseAsync('{"name": "JacksonTian"}', function (err, data) { 37 | should.not.exist(err); 38 | data.name.should.be.equal('JacksonTian'); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('parseAsync should throw err', function (done) { 44 | lib.parseAsync('{"name": "JacksonTian"}}', function (err, data) { 45 | should.exist(err); 46 | done(); 47 | }); 48 | }); 49 | }); 50 | 51 | describe("getContent", function () { 52 | it('getContent should ok', function (done) { 53 | lib.getContent(__filename, function (err, content) { 54 | should.not.exist(err); 55 | content.length.should.be.above(100); 56 | done(); 57 | }); 58 | }); 59 | 60 | describe("mock getCotent", function () { 61 | var _readFile; 62 | before(function () { 63 | _readFile = fs.readFile; 64 | fs.readFile = function (filename, encoding, callback) { 65 | process.nextTick(function () { 66 | callback(new Error("mock readFile error")); 67 | }); 68 | }; 69 | }); 70 | 71 | it("should get mock error", function (done) { 72 | lib.getContent(__filename, function (err, content) { 73 | should.exist(err); 74 | err.message.should.be.equal("mock readFile error"); 75 | done(); 76 | }); 77 | }); 78 | 79 | after(function () { 80 | // 用完之后记得还原。否则影响其他case 81 | fs.readFile = _readFile; 82 | }); 83 | }); 84 | 85 | describe("mock getCotent with muk", function () { 86 | before(function () { 87 | muk(fs, 'readFile', function(filename, encoding, callback) { 88 | process.nextTick(function () { 89 | callback(new Error("mock readFile error with muk")); 90 | }); 91 | }); 92 | }); 93 | 94 | it("should get mock error", function (done) { 95 | lib.getContent(__filename, function (err, content) { 96 | should.exist(err); 97 | err.message.should.be.equal("mock readFile error with muk"); 98 | done(); 99 | }); 100 | }); 101 | 102 | after(function () { 103 | muk.restore(); 104 | }); 105 | }); 106 | }); 107 | 108 | describe('_limit', function () { 109 | var _limit = rewire('../lib/index.js').__get__('limit'); 110 | it('_limit should success', function () { 111 | _limit(10).should.be.equal(10); 112 | }); 113 | 114 | it('_limit should ok when less than 0', function () { 115 | _limit(-1).should.be.equal(0); 116 | }); 117 | }); 118 | 119 | describe('limit2', function () { 120 | it('limit2 should success', function () { 121 | lib.limit2(10).should.be.equal(10); 122 | }); 123 | 124 | it('limit2 should ok when less than 0', function () { 125 | lib.limit(-1).should.be.equal(0); 126 | }); 127 | }); 128 | }); 129 | --------------------------------------------------------------------------------