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

4.7 Jasmine插件

Jasmine作为一款流行的JavaScript单元测试框架,拥有大量可以扩展Jasmine功能的插件。本书介绍jasmine-ajax和jasmine-jquery这两个插件。

4.7.1 jasmine-ajax

JavaScript应用经常使用Ajax将数据发送给远程服务器并接收服务器的响应结果。单元测试需要隔离实际的服务器,并且控制Ajax调用的返回结果。为此Jasmine提供插件jasmine-ajaxPivotal Labs. jasmine-ajax - A library for faking Ajax responses in your Jasmine suite[OL]. 2015. https://github.com/jasmine/jasmine-ajax.来截获Ajax请求并模拟返回结果。

1.安装jasmine-ajax库

使用npm命令安装jasmine-ajax库。

   C:\jasmine-demo>npm install --save-dev jasmine-ajax

安装完毕后目录结构如下:

   -- jasmine-demo
     +-- node_modules
     | +-- jasmine-ajax
     | +-- jasmine-core
     | +-- jquery
     +-- src
     +-- spec

2.引用jasmine-ajax库

jasmine-ajax库文件是mock-ajax.js。为了使用jasmine-ajax插件,需要在测试执行页面jasmine-demo\spec\Plugin\SpecRunner.html里引用mock-ajax.js。其代码如下:

   <script src="../../node_modules/jasmine-ajax/lib/mock-ajax.js"></script>

3.初始化和卸载jasmine-ajax

如果要测试Ajax调用,需要预先调用jasmine.Ajax.install进行初始化(通常在beforeEach里),替换XMLHttpRequest对象。XMLHttpRequest是现代浏览器内建对象,Ajax调用就是通过XMLHttpRequest对象和后端服务器进行数据交换。XMLHttpRequest对象被替换后,当前页面的Ajax调用都会被jasmine-ajax所截获。

测试完毕后需要调用jasmine.Ajax.uninstall以卸载jasmine-ajax(通常在afterEach中),恢复原有的XMLHttpRequest对象。

       describe('jasmine-ajax', function () {
           beforeEach(function () {
             jasmine.Ajax.install();
           });
           afterEach(function () {
             jasmine.Ajax.uninstall();
           });
       });

4.模拟Ajax返回结果

jasmine-ajax截获Ajax请求后,即可用于在任意时间调用respondWith函数返回的结果。示例代码如下:

           it('should specify response when you need it', function () {
               var doneFn = jasmine.createSpy('success');
               var xhr = new XMLHttpRequest();
               xhr.onreadystatechange = function (args) {
                 if (this.readyState == this.DONE) {
                     doneFn(this.responseText);
                 }
               };
               xhr.open('GET', '/site/test/url');
               xhr.send();
               var request = jasmine.Ajax.requests.mostRecent();
               expect(request.url).toBe('/site/test/url');
               expect(doneFn).not.toHaveBeenCalled();
               request.respondWith({
                 'status': 200,
                 'contentType': 'text/plain',
                 'responseText': 'awesome response'
               });
               expect(doneFn).toHaveBeenCalledWith('awesome response');
           });

以上示例先创建一个spy函数doneFn作为Ajax调用结束的回调函数,然后测试用例发出一个Ajax请求。当然这个请求会被jasmine-ajax截获,使用jasmine.Ajax.requests. mostRecent获取这个请求对象。示例代码如下:

       var request = jasmine.Ajax.requests.mostRecent();

jasmine.Ajax.requests会返回RequestTracker对象。以上示例使用了mostRecent成员函数,其他成员可以参考mock-ajax.js里的实现完成:

    function RequestTracker() {
        var requests = [];
        this.track = function(request) {
          requests.push(request);
        };
        this.first = function() {
          return requests[0];
    };
    …

从这个请求对象的属性如url、method、data()等可以获得Ajax请求的相关信息。此时spy函数doneFn还没有被调用,因为此时还没有设置返回结果。

调用请求对象的respondWith函数可以设置返回结果。本示例设置了状态代码(status)、内容类型(contentType)以及返回文本(responseText)。最后验证spy函数doneFn已被调用。

除了利用responseWith函数在需要的时候返回指定内容,也可以预先设置条件。一旦Ajax请求符合这个条件,jasmine-ajax会立即返回预设结果。示例代码如下:

           it('should allow responses to be setup ahead of time', function () {
               var doneFn = jasmine.createSpy('success');
               jasmine.Ajax.stubRequest('/another/url').andReturn({
                 'responseText': 'immediate response'
               });
               var xhr = new XMLHttpRequest();
               xhr.onreadystatechange = function (args) {
                 if (this.readyState == this.DONE) {
                     doneFn(this.responseText);
                 }
               };
               xhr.open('GET', '/another/url');
               xhr.send();
               expect(doneFn).toHaveBeenCalledWith('immediate response');
           });

上面的示例中使用了stubRequest函数设置条件,这样一旦有Ajax请求访问’/another/url', jasmine-ajax立即返回指定结果,即如下代码的作用。

           jasmine.Ajax.stubRequest('/another/url').andReturn({
               'responseText': 'immediate response'
           });

4.7.2 jasmine-jquery

除了测试异步代码,前端JavaScript单元测试的另外一个难点是测试DOM操作。其难点表现在以下两个方面:

1)加载并清除测试所需的DOM元素

为了加载测试所需的DOM元素,需要预先在SpecRunner.html文件里准备DOM元素。例如为前面示例测试Engine.start函数准备了需要淡出的元素:

   <body>
       <div id="fade-div">some content</div>
   </body>

或者在测试用例里动态添加以下代码:

   $('body').append('<div id="fade-div">some content</div>');

但是这样写代码非常烦琐,而且当测试用例执行完毕后,必须手工清除这些DOM元素,恢复测试环境,以免影响其他测试用例的运行。

2)验证DOM元素的变化

执行被测代码后需要验证DOM元素的变化。示例代码如下:

   expect(el.css("display")).toBe("none");

但是Jasmine内建的断言库并没有提供对DOM的特殊支持。针对这个问题,jasmine-jqueryWojciech Zawistowski. jQuery matchers and fixture loader for Jasmine framework[OL]. 2015. https://github. com/velesin/jasmine-jquery.插件提供了API来帮助加载并自动清除HTML、CSS和JSON对象,同时它提供非常强大的自定义匹配器,简化对DOM条件的断言。本书主要介绍HTML对象的加载。如果读者对CSS等技术感兴趣,可以参考此网址https://github.com/velesin/jasmine-jquery的相关内容。

1.安装jasmine-jquery库

使用npm命令安装jasmine-jquery库。

   C:\jasmine-demo>npm install --save-dev jasmine-jquery

安装完毕后目录结构如下:

   -- jasmine-demo
     +-- node_modules
     | +-- jasmine-ajax
     | +-- jasmine-core
     | +-- jasmine-jquery
     | +-- jquery
     +-- src
     +-- spec

2.引用jasmine-jquery库

jasmine-jquery的库文件是jasmine-jquery.js。在测试执行页面jasmine-demo\spec\Plugin\SpecRunner.html里引用jasmine-jquery.js的代码如下:

   <script src="../../node_modules/jasmine-jquery/lib/jasmine-jquery.js"></script>

3.加载HTML DOM元素

在jasmine-demo\src\Plugin\domfunctions.js中准备一个JavaScript函数用来改变DOM元素里的内容,作为被测代码:

   /* domfunctions.js */
   function changeContainerText(txt) {
       if (typeof txt === 'undefined') {
         txt = 'hello world';
       }
       $("#container").text(txt);
   }

所以在测试执行页面jasmine-demo\spec\Plugin\SpecRunner.html里引用这个js文件:

   <script src="../../src/Plugin/domfunctions.js"></script>

测试这个函数需要准备一个id是container的div。jasmine-jquery提供了多种方式加载DOM元素,如表4-6所示。

表4-6 jasmine-jquery HTML DOM加载函数

表4-6提到的“容器”指的是执行单元测试时jasmine-jquery会在测试执行页面SpecRunner.html里动态创建id为jasmine-fi xtures的div元素:

     <div id="jasmine-fixtures">    </div>

jasmine-jquery加载的DOM元素会被插入到这个div元素里。

这个“容器”在测试用例结束后会被自动清除,不会影响下一个测试用例的运行。

表4-6里的全局函数都是jasmine.getFixtures()返回对象里的成员函数的简化形式:

     loadFixtures()⇒jasmine.getFixtures().load()
     appendLoadFixtures()⇒jasmine.getFixtures().appendLoad()
     readFixtures()⇒jasmine.getFixtures().read()
     setFixtures()⇒jasmine.getFixtures().set()
     appendSetFixtures()⇒jasmine.getFixtures().appendSet()

使用setFixtures函数直接加载HTML代码片段:

       describe('setFixtures', function () {
           beforeEach(function () {
             setFixtures('<div id="container"></div><button id="btn" onclick="changeContainer
   Text()">Click</button>');
           });
       });

如果HTML片段比较长的话,可以将HTML片段保存到文件中(例如将以上setFixtures函数里的片段保存到jasmine-demo\spec\Fixtures\PluginFixture.html文件),调用loadFixtures函数进行加载:

       describe('loadFixtures', function () {
           beforeEach(function () {
             var path = '';
             if (typeof window.__karma__ ! == 'undefined') {
                 path += 'base';
             }
             jasmine.getFixtures().fixturesPath = path + '/spec/Fixtures';
             loadFixtures('PluginFixture.html');
           });
       });

jasmine-jquery默认会从spec/javascripts/fixtures目录加载HTML文件。这个默认路径可以被修改,例如:

     jasmine.getFixtures().fixturesPath = 'my/new/path';

注意:如果在单元测试里使用Karma的话,测试文件会从base/目录里被访问,所以修改jasmine-jquery默认路径时需要加上base/前缀:

     jasmine.getFixtures().fixturesPath = 'base/my/new/path';

loadFixture函数会使用Ajax调用来加载HTML文件,因为浏览器默认不允许Ajax加载本地文件。如果在本地直接打开SpecRunner.html文件,以上的示例将无法运行,所以需要使用一个HTTP服务器,并且将HTTP服务器的根目录设为jasmine-demo。这样通过HTTP服务器访问SpecRunner.html,即可使loadFixture成功加载/Spec/Fixtures/PluginFixture.html。

使用npm命令可以全局安装一个简易的HTTP服务器:

   npm install http-server -g

然后在命令控制台将当前目录切换到jasmine-demo,运行以下命令:

   http-server

这个简易HTTP服务器默认使用8080端口,所以需访问http://localhost:8080/spec/plugin/specrunner.html进行单元测试。

jasmine-ajax会截获所有的Ajax请求,而jasmine-jquery的loadFixture会调用Ajax加载HTML文件,所以当两者一起使用的时候,必须先调用loadFixture,然后再初始化jasmine-ajax。例如:

     beforeEach(function(){
       // first load your fixtures
       loadFixtures('fixture.html');
       // then install the mock
       jasmine.Ajax.install();
     });

4.验证DOM元素的变化

jasmine-jquery为jQuery框架提供了大量自定义匹配器,如表4-7所示。为了测试以上示例里changeContainerText函数是否更改DOM成功,可以使用以下的测试用例:

表4-7 常用jasmine-jquery自定义匹配器

       it('container should have hello world text', function () {
           expect($('#btn')).toExist();
           $('#btn').trigger('click');
           expect($('#container')).toHaveText('hello world');
       });

读者可以访问https://github.com/velesin/jasmine-jquery了解更多的jasmine-jquery匹配器。