写完才发现,到底是单元测试框架还是单元测试函数,是个问题?毕竟才

不到 50 行。

上一篇看 Webpack 源码的然后写了个仿 tape,这里来理理如何用 Promise 实现单元测试。

1
2
3
4
test('A passing test', (assert) => {
assert.pass('This test will pass.');
assert.end();
});

例子是这样的,有那么一个 test 方法,接受一个 Label 和函数作为参数。

并且有如下约束,end 必须执行,且前面的所有断言均为真,那就需要一个变量来储存这个状态。

单元测试函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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();
});

运行一下,会获得如下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ 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 似乎少了什么,来对比一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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,那我们换个例子。

1
2
3
4
test('Throw a error', (assert) => {
assert.fail('Bomb');
assert.end();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ 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 的左查询(赋值操作)有个特性,会先再作用域里面查找,然后替换,常用于惰性求值。于是有了如下代码,用个子函数作为执行体,外部通过闭包保存全局数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 就可以放进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,输出如下结果。

1
2
3
4
5
6
7
8
$ 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 事件。

最终实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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);
}

测试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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

其实这得算 0.2 了,之前那个版本在这 使用 Promise 实现的单元测试框架??函数,逻辑一样,但是长了好多。

单元测试框架

既然称之为框架,自然要有自己的 Cli 接口,能对项目级的测试进行处理。

所以需要拓展上面的测试函数为如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env node

function test(label, fn) {
...
}

module.exports = (label, fn) => test(label, fn);

if (!module.parent) {

const cwd = process.cwd();
const fs = require('fs');
const path = require('path');

delete require.cache[module.filename];
let _nodeModulePaths = module.__proto__.constructor._nodeModulePaths;
module.__proto__.constructor._nodeModulePaths = (...args) => {
let paths = _nodeModulePaths(...args);
paths.unshift(module.path);
return paths
}

process.argv
.slice(2)
.map(p => fs.readdirSync(p).map(file => path.join(cwd, p, file)))
.flat()
.map(tests => require(tests));
}

一方面,module.exports 将其导出,可以通过 require('../path/test') 调用;另一方面,module.__proto__.constructor._nodeModulePaths hack 了 node 的模块加载机制,将当前文件加入模块目录,可以通过 require('test');而之后调用 require 方法,执行传入目录下所有测试项目。

1558061315454.png
1558061315454.png

对于如上目录结构的项目,可以这样进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ node ./bin/test.js ./tests
TinyTest version 0.2

1. Fail
[err] Error: fail
at Object.fail (C:\Users\Administrator\Desktop\transer\bin\test.js:27:83)
at C:\Users\Administrator\Desktop\transer\tests\fail.js:4:12
at C:\Users\Administrator\Desktop\transer\bin\test.js:32:13
at new Promise (<anonymous>)
at rawTest (C:\Users\Administrator\Desktop\transer\bin\test.js:22:9)
at test (C:\Users\Administrator\Desktop\transer\bin\test.js:39:27)
at test (C:\Users\Administrator\Desktop\transer\bin\test.js:40:5)
at module.exports (C:\Users\Administrator\Desktop\transer\bin\test.js:43:33)
at Object.<anonymous> (C:\Users\Administrator\Desktop\transer\tests\fail.js:3:1)
at Module._compile (internal/modules/cjs/loader.js:759:30)

2. A passing test
[ok] This test will pass.

3. Throw a error
[err] Error: Bomb.
at C:\Users\Administrator\Desktop\transer\tests\throw.js:4:11
at C:\Users\Administrator\Desktop\transer\bin\test.js:32:13
at new Promise (<anonymous>)
at rawTest (C:\Users\Administrator\Desktop\transer\bin\test.js:22:9)
at test (C:\Users\Administrator\Desktop\transer\bin\test.js:39:27)
at module.exports (C:\Users\Administrator\Desktop\transer\bin\test.js:43:33)
at Object.<anonymous> (C:\Users\Administrator\Desktop\transer\tests\throw.js:3:1)
at Module._compile (internal/modules/cjs/loader.js:759:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10)
at Module.load (internal/modules/cjs/loader.js:628:32)

# pass 1/3
# fail 2/3