使用 Promise 实现的单元测试框架

178 阅读3分钟
原文链接: gitai.me

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

不到 50 行。

上一篇看 Webpack 源码的然后写了个仿 tape,这里来理理如何用 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

完美收工。

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