JavaScript 的异步测试

2010-11-26 07:50

JavaScript 的异步测试

by lifesinger

at 2010-11-25 23:50:40

original http://lifesinger.org/blog/2010/11/js-async-test/

有这样一段代码:

var O = {
    data: 1,
    fn: function(callback) {
        setTimeout(function() {
            O.data++;
            callback();
        }, 100);
    },
    fn2: function() {
        O.data--;
    }
};

现在要测试 O 的两个方法是否正常工作。我们采用自主开发的测试框架 STF:

/* STF - stupid test framework v0.1 */
function test(desc, fn) {
    console.log('START test ' + desc);
    fn && fn();
    console.log('END test ' + desc);
}
function equals(actual, expected) {
    console.log(actual === expected ? '  passed' : '  failed');
}

有了以上方法,很容易写出测试用例:

test('fn', function() {
    O.data = 1;
    O.fn(function() {
        equals(O.data, 2);
    });
});
test('fn2', function() {
    O.data = 1;
    O.fn2();
    equals(O.data, 0);
});

一运行,输出的 log 顺序很乱,有个用例还失败了:

START test fn
END test fn
START test fn2
  passed
END test fn2
  failed

都是异步惹的祸

对上面的结果稍加分析可以看出,一切都是 setTimeout 惹的祸。
只要被测试的代码里有异步操作,就有可能引起上面的问题。作为一个非常有前景的 STF 测试框架,究竟怎样才能支持异步测试呢?

思路很简单,如果没有异步,都是同步运行,那就很好测试了。因此立刻推出 STF v0.2:

// STF 里增加 sleep 方法
function sleep(ms) {
    var t = (new Date()).getTime();
    while((new Date()).getTime() - t < ms);
}

将 O.fn 的测试用例修改为:

test('fn', function() {
    O.data = 1;
    O.fn(function() {
        equals(O.data, 2);
    });
    sleep(200);
});

满怀期望一运行,杯具,log 还是乱的,依旧失败,囧。
分析 sleep 的实现,很明显忽略了 js 单线程的特点,这样实现,是很难搞定的。

解铃还需系铃人

中国有部很有名的电影:《圆环套圆环》,导演和情节我全忘了,但这片名的精髓可以用来解决异步测试。
有了实现思路,就好办了。立刻升级 STF 到 v0.9:

/* STF - stupid test framework v0.9 */
var blocks = [];
function test(desc, fn) {
    blocks.push(function() { console.log('START test ' + desc); });
    fn();
    blocks.push(function() { console.log('END test ' + desc); });
}
function runs(fn) {
    blocks.push(fn);
}
function waits(ms) {
    blocks.push(ms);
}
function equals(actual, expected) {
    console.log(actual === expected ? '  passed' : '  failed');
}
function run(fns) {
   var b;
    fns = fns || blocks;
    while((b = fns.shift())) {
        if(typeof b !== 'number') {
            b();
        } else {
            setTimeout(function() {
                run(fns);
            }, b);
            break;
        }
    }
}

核心思想是将测试代码封装成一个个 block 块,然后依次执行。碰到异步操作时,后续 block 块都放到异步操作的回调里。这样“圆环套圆环”,就能保证“同步”执行了。

测试用例修改为:

test('fn', function() {
    runs(function() {
        O.data = 1;
        O.fn(function() {
            equals(O.data, 2);
        });
    });
    waits(200);
});
test('fn2', function() {
    runs(function() {
        O.data = 1;
        O.fn2();
        equals(O.data, 0);
    });
});
run();

来看运行结果:

START test fn
  passed
END test fn
START test fn2
  passed
END test fn2

非常棒,一切如预期。

注意,以上 STF 框架最多仅是 v0.9, 离正式版 v1.0 还差了至少一光年距离。考虑更完善、功能更强大的 JS 测试框架,推荐 Jasmine. 本文仅是对 Jasmine 异步测试原理的简陋模拟。

Jasmine 相关链接:源码 | 文档

打造一个测试框架不难,难的是思路和想法,以及疯狂的细节和对完美的追求。
希望这篇文章能让你对 Jasmine 的异步测试原理有个初步了解。

晚安,睡觉。