背景说明
最近因工作上的原因,研究了一下JavaScript测试框架Jasmine。和其他的单元测试框架(比如JUnit等)类似,编写单元测试并不复杂,困难的是弄清楚被测试对象的输入输出,这里就不赘述了。在编写测试用例过程中碰到一个难题:某些JavaScript方法比较耗时,所以采用了异步方式,但和我们平常使用回调函数的方式不同,通过发送系统消息的方式通知,系统中定义了很多消息代码,通过消息代码而得知JavaScript方法执行的结果。因为在实际工作环境中,浏览器经过了定制,为了能够在通用浏览器中演示这一解决方法,下面使用window.postMessage代替实际的代码来说明。
function async_call(data) {
window.postMessage(data, '*');
}
解决过程
看到这个方法,首先想到的是注册一个事件监听,在异步方法后面等待一个时间间隔,然后判断接收到的消息:
describe("WrongAsyncTest", function() {
var code;
var foo = {
onmessage: function(e) {
code = e.data;
}
};
beforeEach(function() {
window.addEventListener('message', function(e) {
foo.onmessage(e);
}, false);
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('testAsyncCall', function() {
async_call('10001');
jasmine.clock().tick(1000);
expect(code).toEqual('10001');
});
});
实际上这是行不通的,虽然我们在async_call后面等待了足够的时间,但我们要知道javascript代码实际上是运行于单线程中的,所以不管在testAsyncCall这个方法中等待多长时间,foo.onmessage都在testAsyncCall执行完成之后才会被调用,所以在expect(code).toEqual(‘10001’)这句断言中,code的值实际上还未定义,得到的结果就是:
Expected undefined to equal '10001'.
此路不通,再看jasmine的文档,其中有一部分讲的就是Asynchronous Support。在jasmine的beforeAll, afterAll, beforeEach, afterEach和it调用中,可以增加一个可选的参数(实际上就是一个回调函数),在异步方法执行完成后,调用这个回调函数,通知jasmine执行下一个测试方法。初听起来似乎有点不知所云,还是用一个例子说明:
describe("Synschronous specs", function() {
var value;
beforeEach(function(done) {
setTimeOut(function() {
value = 0;
done();
}, 1);
});
it("first", function() {
expect(value).toEqual(0);
});
在这个例子中,beforeEach增加了一个done参数,指示Jasmine在done回调函数没调用前,不执行下一个测试方法,所以first测试会一直等待beforeEach中的定时器超时,执行了done()之后才会得到执行。
解决方案
有了这种机制,针对async_call的测试用例就好写了,代码如下:
describe("AsyncTest", function() {
var code;
var foo = {
setcode: function(e) {
code = e.data;
},
onmessage: function(e) {
console.log("onmessage");
}
}
window.addEventListener('message', function(e) {
foo.onmessage(e);
}, false);
beforeEach(function(done) {
spyOn(foo, "onmessage").and.callFake(function(e) {
foo.setcode(e);
done();
});
async_call('10001');
});
it('testAsyncCall', function() {
expect(foo.onmessage).toHaveBeenCalled();
expect(code).toEqual('10001');
});
});
说明,done是beforeAll, afterAll, beforeEach, afterEach和it调用的可选参数,所以并不能在foo.onmessage中调用,于是代码中采用了一个迂回的方案,借助于spyOn来监控函数是否被调用。and.callFake的含义是,在被监控的函数被调用时,将用所提供的函数取代被监控函数。就上面的代码而言,就是foo.onmessage被调用时,使用
function(e) {
foo.setcode(e);
done();
}
替代这个调用。为什么要增加一个foo.setcode方法,那是因为翻看了jasmine文档,没有找到被监控函数调用后,附加执行一个函数的方法,所以增加了一个foo.setcode方法。
总结
Jasmine对javascript同步和异步方法测试都提供了良好的支持,可以用来编写JS单元测试用例。借助第三方js扩展库,还可以测试DOM事件、将测试结果保存到文件等等,这等待下次进一步分析。
上述示例代码可以在我的github项目jsdemo中找到。
参考文档
1. Jasmine官方文档
2. Using Jasmine 2.0’s New done() Function to Test Asynchronous Processes