4.6 测试异步代码
虽然JavaScript的执行环境是单线程的,但是JavaScript支持异步模式,有很多通过回调函数来执行的异步调用,例如setTimeout或setInterval。JavaScript单元测试需要考虑测试以下3种异步代码:
• 包含调用setTimeout或setInterval的代码。
• 需要花费一点时间才能显示的界面效果,例如网页元素的淡进淡出。
• Ajax调用。(在下一节将介绍使用Jasmine插件进行测试的方法)
为了演示测试异步代码,在jasmine-demo\src\Async目录下创建Engine.js。以下是被测的JavaScript代码。
/* Engine.js */ var Engine = function(displayElement) { this.$el = $(displayElement); }; Engine.prototype.start = function(cb) { this.$el.fadeOut(1000, cb); };
以上代码中,start函数里使用jQuery的fadeOut函数实现指定页面元素的淡出效果,时长为1秒(1000毫秒),元素淡出后会执行回调函数cb。
在jasmine-demo\spec\Async目录下创建Engine_spec.js文件,添加以下测试代码:
/* Engine_spec.js */ describe('Engine', function () { describe('UI Tests', function () { var engine, el; beforeEach(function () { el=$('#fade-div'); engine = new Engine(el); engine.start(); }); it('should work with a visual effect', function () { expect(el.css("display")).toBe("none"); }); }); });
以上示例使用了jQuery获取页面中的fade_div元素,在测试用例中调用engine.start,然后验证fade_div元素是否消失。
因为使用了jQuery,所以SpecRunner.html需要引用jQuery库,同时也预先准备了fade_div元素,供测试使用。该.html文件内容如下:
<! DOCTYPE html> <html lang="en"> <head> … <script src="../../node_modules/jquery/dist/jquery.min.js"></script> <! -- include source files here... → <script src="../../src/Async/Engine.js"></script> <! -- include spec files here... → <script src="Engine_spec.js"></script> </head> <body> <div id="fade-div">some content</div> </body> </html>
双击SpecRunner.html文件,默认浏览器执行测试代码并且报告测试失败,如图4-14所示。
图4-14 Engine单元测试失败报告
测试失败的原因是engine.start里使用的fadeOut是一个异步调用,页面元素要在1秒后才消失。但是在测试用例里,engine.start函数执行后马上检验页面元素是否消失,此时测试页面元素仍旧存在,所以测试失败。例如:
engine.start(); {…} expect(el.css("display")).toBe("none");
为了测试以上的异步代码,需要用到Jasmine的异步支持。
4.6.1 Jasmine的异步支持
为了支持异步代码的测试,Jasmine的全局函数beforeAll、afterAll、beforeEach、afterEach和it的回调函数(代码块)有一个可选参数done。例如:
describe("Asynchronous specs", function () { var value; beforeEach(function (done) { setTimeout(function () { value = 0; done(); }, 1000); }); it("should support async execution", function (done) { … }); });
参数done用于通知Jasmine:这里有一个异步函数,必须等到done()被调用后才能结束当前测试步骤(beforeAll、afterAll、beforeEach、afterEach和it),再继续执行下一步的测试代码。所以在上面的示例中,Jasmine会在beforeEach里等待1秒(1秒后done被调用),然后再执行后续的it函数。
默认情况下Jasmine在每一步骤(beforeAll、afterAll、beforeEach、afterEach和it)里最多等待5秒。如果5秒后done还是没有被调用,当前的测试用例会被标记为失败,然后Jasmine继续执行下一测试步骤。
开发人员可以修改全局的默认超时时间(例如修改为2秒),代码如下:
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
或者在beforeAll、afterAll、beforeEach、afterEach和it这些函数中传入一个额外的超时设置,单独调整这一步骤的超时时间。例如:
beforeEach(function (done) { setTimeout(function () { value = 0; done(); }, 1000); }, 2000);
如果想要手工标记测试用例为失败,可以调用done.fail()函数。
了解了Jasmine对测试异步代码的支持后,前面Engine_spec.js里的测试用例就可以改写为:
beforeEach(function (done) { el = $('#fade-div'); engine = new Engine(el); engine.start(function () { done(); }); });
在本示例中,fadeOut(engine.start)可以接受外部输入的回调函数,所以在回调函数里调用done告诉JasminefadeOut已经执行完毕,可以接下来执行下一步骤(it块)进行验证。
如果fadeOut或engine.start不接受外部的回调函数,那么可以使用setTimeout或setInterval延迟一段时间再进行验证。示例代码如下:
it('should work with a visual effect', function (done) { setTimeout(function() { expect(el.css("display")).toBe("none"); done(); }, 2000); });
4.6.2 模拟JavaScript Timeout相关函数
虽然以上方法可以测试异步代码,但是单元测试需要尽可能地快速运行,如果测试用例中频繁出现setTimeout或setInterval,那么大量时间会被浪费在等待上,造成测试效率低下。为此Jasmine提供了一个虚拟时钟,模拟JavaScript Timeout相关函数。
为了演示Jasmine的虚拟时钟,先在Engine.js里为Engine添加一个新方法:
Engine.prototype.pause10seconds = function(cb) { setTimeout(cb, 10000); };
虽然可以按前面所述的方案在pause10seconds的回调函数里检验测试结果,但是该方法需要等待10秒,测试效率很低。为了让测试时间“快进”,需要使用Jasmine的虚拟时钟。其用法是:
(1)调用jasmine.clock().install函数安装这个虚拟时钟(通常在beforeEach里)。
(2)调用jasmine.clock().tick函数“快进”时间,这个函数接受的参数是毫秒数。然后验证回调函数的结果。
(3)测试完毕后需要调用jasmine.clock().uninstall函数卸载虚拟时钟(通常在afterEach里)。
测试用例如下:
describe('Clock Tests', function () { beforeEach(function () { jasmine.clock().install(); }); afterEach(function () { jasmine.clock().uninstall(); }); it('should callback after 10 seconds', function () { var engine = new Engine(); var timerCallback = jasmine.createSpy("timerCallback"); engine.pause10seconds(timerCallback); jasmine.clock().tick(8000); expect(timerCallback).not.toHaveBeenCalled(); jasmine.clock().tick(2050); expect(timerCallback).toHaveBeenCalled(); }); });
以上示例里先使用jasmine.createSpy创建一个spy函数,然后执行被测代码engine. pause10seconds,理论上需要等待10秒spy函数才会被执行。调用jasmine.clock().tick先“快进”8秒,这时验证spy函数没有被调用过。再次调用jasmine.clock().tick“快进”2秒多,此时虚拟时钟已经过了10秒,spy函数执行完毕。
此示例没有使用done,但是利用Jasmine虚拟时钟“快进”了时间,使得setTimeout和setInterval的回调函数以同步的方式被执行。
Jasmine虚拟时钟的“快进”功能极大地提高了单元测试的效率。