Web前端测试与集成:Jasmine/Selenium/Protractor/Jenkins的最佳实践
上QQ阅读APP看书,第一时间看更新

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延迟一段时间再进行验证sheelc. testing fadeOut() method[OL]. 2014. https://github.com/jasmine/jasmine/issues/516.。示例代码如下:

       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虚拟时钟的“快进”功能极大地提高了单元测试的效率。