写完才发现,到底是单元测试框架还是单元测试函数,是个问题?毕竟才不到 50 行。这里来理理如何用 Promise 实现单元测试。
test('A passing test', (assert) => { assert.pass('This test will pass.'); assert.end(); }); |
例子是这样的,有那么一个 test 方法,接受一个 Label 和函数作为参数。
并且有如下约束,end 必须执行,且前面的所有断言均为真,那就需要一个变量来储存这个状态。
function test(label, fn) { let handle = (err, msg) => { console.log(label); if (err) { console.log(`[err]`, err); return; } console.log(`[ok] ${msg}`); }; return new Promise((resolve, reject) => { let ok = false; let data = null; let assert = { pass: (msg) => (ok = true, data=msg), throw: (err) => (ok = false, data=err), end () { ok ? resolve (data) : reject (data); } }; fn (assert); ok = false; assert.end(); }) .then((msg) => handle(null, msg), (err) => handle(err)) } test('A passing test', (assert) => { assert.pass('This test will pass.'); assert.end(); }); test('Throw a error', (assert) => { throw new Error('Bomb.'); assert.end(); }); |
运行一下,会获得如下结果
$ node ./src/test.js A passing test [ok] This test will pass. Throw a error [err] Error: Bomb. at test (C:\Users\Administrator\Desktop\transer\src\test.js:35:9) at Promise (C:\Users\Administrator\Desktop\transer\src\test.js:21:5) at new Promise (<anonymous>) at test (C:\Users\Administrator\Desktop\transer\src\test.js:11:10) at Object.<anonymous> (C:\Users\Administrator\Desktop\transer\src\test.js:34:1) at Module._compile (internal/modules/cjs/loader.js:701:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10) at Module.load (internal/modules/cjs/loader.js:600:32) at tryModuleLoad (internal/modules/cjs/loader.js:539:12) at Function.Module._load (internal/modules/cjs/loader.js:531:3) |
完美,但是比起 tape 似乎少了什么,来对比一下。
$ node ./src/test.js TAP version 13 # A passing test ok 1 This test will pass. # Throw a error C:\Users\Administrator\Desktop\transer\src\test.js:8 throw new Error('Bomb.'); ^ Error: Bomb. at Test.test (C:\Users\Administrator\Desktop\transer\src\test.js:8:9) at Test.bound [as _cb] (C:\Users\Administrator\Desktop\transer\node_modules\tape\lib\test.js:77:32) at Test.run |
哦,他居然不能捕获 throw,那我们换个例子。
test('Throw a error', (assert) => { assert.fail('Bomb'); assert.end(); }); |
$ node ./src/test.js TAP version 13 # A passing test ok 1 This test will pass. # Throw a error not ok 2 Bomb --- operator: fail at: Test.test (C:\Users\Administrator\Desktop\transer\src\test.js:8:10) stack: |- Error: Bomb at Test.assert [as _assert] (C:\Users\Administrator\Desktop\transer\node_modules\tape\lib\test.js:226:54) at Test.bound [as _assert] (C:\Users\Administrator\Desktop\transer\node_modules\tape\lib\test.js:77:32) at Test.fail ... 1..2 # tests 2 # pass 1 # fail 1 |
相比他,我们的实在是太完美了,就差个报告。
首先是最前面的 TAP version 13 我们要写个方法,让他只出现一次。必须写在一个全局唯一的实例里面,还不能污染其他环境。
JavaScript 的左查询(赋值操作)有个特性,会先再作用域里面查找,然后替换,常用于惰性求值。于是有了如下代码,用个子函数作为执行体,外部通过闭包保存全局数据。
function test (label, fn) { console.log('TinyTest version 0.1'); function rawTest (label, fn) { let handle = (err, msg) => { // ... }; new Promise((resolve, reject) => { // ... }) .then((msg) => handle(null, msg), (err) => handle(err)); } test = (label, fn) => rawTest(label, fn); test(label, fn); } |
运行到结束时,会用箭头表达式,生成匿名函数覆盖上面定义的 test。
这样就能保证 'TinyTest version 0.1' 只会输出一遍。
接下来,既然有了闭包,我们可以在里面存全局变量,那么 id 就可以放进去。
function test (label, fn) { let id = 0; console.log('TinyTest version 0.2') function rawTest (label, fn, id) { let handle = (err, msg) => { console.log(id, label); // ... }; new Promise((resolve, reject) => { // ... }) .then((msg) => handle(null, msg), (err) => handle(err)); } test = (label, fn) => rawTest(label, fn, ++id); test(label, fn); } |
这样就可以得到自增 id,输出如下结果。
$ node ./src/test.js TinyTest version 0.1 1 'A passing test' [ok] This test will pass. 2 'Throw a error' [err] Error: Bomb. at test (C:\Users\Administrator\Desktop\transer\src\test.js:41:9) at Promise (C:\Users\Administrator\Desktop\transer\src\test.js:24:7) |
最后就是那个成功失败数量的报告了,通过变量覆盖确定第一次执行容易,但是确定最后一个执行就难了。
这里可以用 Node.js 生命周期里面的,exit 事件。
最终实现如下:
function test (label, fn) { let id = 0; let success = 0; process.on('exit', () => { console.log(`# pass ${success}/${id}`); console.log(`# fail ${id - success}/${id}`); }); console.log('TinyTest version 0.2') function rawTest (label, fn, id) { let handle = (err, msg) => { console.log(id, label); if (err) { console.log(`[err]`, err); return; } success++; console.log(`[ok] ${msg}`); }; new Promise((resolve, reject) => { let ok = false; let data = null; let assert = { pass: (msg) => (ok = true, data=msg), throw: (err) => (ok = false, data=err), end () { ok ? resolve (data) : reject (data); } }; fn (assert); ok = false; assert.end(); }) .then((msg) => handle(null, msg), (err) => handle(err)); } test = (label, fn) => rawTest(label, fn, ++id); test(label, fn); } |
测试如下:
$ node ./src/test.js TinyTest version 0.2 1 'A passing test' [ok] This test will pass. 2 'Throw a error' [err] Error: Bomb. at test (C:\Users\Administrator\Desktop\transer\src\test.js:47:9) at Promise (C:\Users\Administrator\Desktop\transer\src\test.js:30:7) at new Promise (<anonymous>) at rawTest (C:\Users\Administrator\Desktop\transer\src\test.js:20:5) at test (C:\Users\Administrator\Desktop\transer\src\test.js:37:25) at Object.<anonymous> (C:\Users\Administrator\Desktop\transer\src\test.js:46:1) at Module._compile (internal/modules/cjs/loader.js:701:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10) at Module.load (internal/modules/cjs/loader.js:600:32) at tryModuleLoad (internal/modules/cjs/loader.js:539:12) # pass 1/2 # fail 1/2 |
完美收工。
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。