Angular (二) Jasmine单元测试和端到端测试

单元测试
是一种能够帮助开发者验证代码中某一部分有效性的技术。
端到端测试(E2E)
则是在当你想要确保一堆组件能够按照预先设想的方式整合起来运行的时候使用。作为一个现代的JavaScript MVW 框架对于单元测试和端到端测试都提供了充分的支持。在编写AngularJS应用的同时编写测试能够为你省下很多在未来需要修改bug的时间。本文将介绍如何在AngularJS中编写单元测试和端到端测试。

使用Jasmine作为测试框架并使用karma作为测试运行期

如果你现在还没有一个测试环境,你应该首先完成以下的步骤:

下载并安装Node.js
使用npm安装karma(npm install -g karma)
从Github上下载本文的demo并完成解压
在解压后的文件中,你可以的test/unit和test/e2e目录下找到测试文件。为了查看测试结果,你可以运行 script/test.bat 文件,它将会开启Karma服务器。我们的主要HTML文件时app/notes.html,你可以通过 https://localhost/angular-seed/app/notes.html 访问。

开始单元测试

首先,我们会创建一个简单的Angular应用,来看看如何将单元测试加入开发过程中。因此,我们创建一个应用并将单元测试运用到各个组件中。在这一部分我们将学习如何编写单元测试:

Controller
Directives
Filters
Factories
我们将会创建一个非常简单的todo应用。我们标记将会包含一个文本输入框,用户可以在这里编写一些简单的笔记。当用户按下一个按钮时,这个笔记就会被添加到一个笔记列表中。我们将使用HTML的LocalStorage来储存笔记信息。初始的HTML代码如下所示。我们同事会使用Bootstrap来创建布局。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html ng-app="todoApp">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.min.js" type="text/javascript"></script>
    <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js" type="text/javascript"></script>
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" type="text/css"/>
    <script type="text/javascript" src="js/app.js"></script>
    <style>
      .center-grey{
             background:#f2f2f2;
             margin-top:20;
      }
      .top-buffer {
              margin-top:20px; 
      }
      button{
              display: block; 
              width: 100%;
      }
    </style>
    <title>Angular Todo Note App</title>
  </head>
  <body>
    <div class="container center-grey" ng-controller="TodoController">
      <div class="row top-buffer" >
        <span class="col-md-3"></span>
        <span class="col-md-5">
          <input class="form-control" type="text" ng-model="note" placeholder="Add a note here"/> 
        </span>
        <span class="col-md-1">
          <button ng-click="createNote()" class="btn btn-success">Add</button>
        </span>
        <span class="col-md-3"></span>
      </div>
      <div class="row top-buffer" >
        <span class="col-md-3"></span>
        <span class="col-md-6">
          <ul class="list-group">
            <li ng-repeat="note in notes track by $index" class="list-group-item">
              <span>{{note}}</span>
            </li>
          </ul>
        </span>
        <span class="col-md-3"></span>
      </div>
    </div>
  </body>
</html>

在上面的代码中,正如你所见,我们的Angular 模块是todoApp,控制器时TodoController。输入框被绑定到了note模型。同时也有一个列表展示已经被添加的笔记系那个吗。另外,当按钮被点击时,TodoController的createNote()函数会被调用,现在我们打开app.js文件,来创建模型和控制器。将夏敏的代码添加到app.js文件中:

var todoApp = angular.module('todoApp',[]);

todoApp.controller('TodoController', function($scope, notesFactory) {
  $scope.notes = notesFactory.get();
  $scope.createNote = function() {
    notesFactory.put($scope.note);
    $scope.note = '';
    $scope.notes = notesFactory.get();
  }
});

todoApp.factory('notesFactory', function() {
  return {
    put: function(note) {
      localStorage.setItem('todo' + (Object.keys(localStorage).length + 1), note);
    },
    get: function() {
      var notes = [];
      var keys = Object.keys(localStorage);

      for(var i = 0; i < keys.length; i++) {
        notes.push(localStorage.getItem(keys[i]));
      }

      return notes;
    }
  };
});

我们的TodoController使用了一个叫做notesFactory的factory服务来储存和提取笔记信息。当createNote()函数被调用时,它会使用这个factory服务来将一条信息存进localStorage中,然后清空note模型。因此,如果我们进行单元测试时,我们应该确保当控制器初始化时,scope包含了一定数量的笔记。在调用createNote()函数之后,笔记的数量应该比之前的数量加一。我们进行单元测试的代码如下所示:

describe('TodoController Test', function() {
  beforeEach(module('todoApp')); // will be run before each it() function

  // we don't need the real factory here. so, we will use a fake one.
  var mockService = {
    notes: ['note1', 'note2'], //just two elements initially
    get: function() {
      return this.notes;
    },
    put: function(content) {
      this.notes.push(content);
    }
  };

  // now the real thing: test spec
  it('should return notes array with two elements initially and then add one',
    inject(function($rootScope, $controller) { //injects the dependencies
      var scope = $rootScope.$new();

      // while creating the controller we have to inject the dependencies too.
      var ctrl = $controller('TodoController', {$scope: scope, notesFactory:mockService});

      // the initial count should be two
      expect(scope.notes.length).toBe(2);

      // enter a new note (Just like typing something into text box)
      scope.note = 'test3';

      // now run the function that adds a new note (the result of hitting the button in HTML)
      scope.createNote();

      // expect the count of notes to have been increased by one!
      expect(scope.notes.length).toBe(3);
    })
  );
});

describe()方法定义了一个测试套件。它的作用是将所有测试包含在这个套件中。其中我们在所有的it()之前执行了一个beforeEach()函数。每一个it()函数是我们的测试spec,其中进行的是真正的测试。因此,在所有的测试被执行之前,我们需要载入我们的模块。

由于这是一个单元测试,我们不需要外部依赖。你已经知道了我们的控制器依赖于noteFactory处理笔记。因此,为了对控制器进行单云测试我们需要使用一个假的factory或service。这就是我们为什么要创造mokeService的原因,它用来模拟真是的noteFactory,其中包含相同的函数,get()和put()。虽然我们真正的factory使用localStorage来存储笔记,这个假的factory使用一个内嵌的数组。

现在,我们来看看it()函数究竟是怎么被用来进行测试的。你会看到它声明了两个依赖项目 rootScope controller,它们都可以由Angular自动注入。两个服务被用来获取根作用域和创建新控制器。

controller rootScope.$new()方法将会返回一个新的自作用于,它用来注入控制器。注意到我们同事也为控制器传递了一个我们定义的假factory。

现在,expect(scope.notes.length).toBe(2)会在控制器初始化scope.notes的时候进行断言。如果笔记的数目多于或者少于两个,测试将会失败。类似的,我们产生了一个新的note模型,然后运行了createNote()函数,我们预期它将会增加一个新的笔记项目。现在expect(scope.notes.length).toBe(3)会检查结果。由于我们在初始化的时候添加了两个项目,现在应该有3个项目。你可以在karma中查看测试成功还是失败。

测试Factory

现在我们想要对factory进行单元测试来确保它能够像预想中一样运行。测试的代码如下所示:

describe('notesFactory tests', function() {
  var factory;

  // excuted before each "it()" is run.
  beforeEach(function() {
    // load the module
    module('todoApp');

    // inject your factory for testing
    inject(function(notesFactory) {
      factory = notesFactory;
    });

    var store = {
      todo1: 'test1',
      todo2: 'test2',
      todo3: 'test3'
    };

    spyOn(localStorage, 'getItem').andCallFake(function(key) {
      return store[key];
    });

    spyOn(localStorage, 'setItem').andCallFake(function(key, value) {
      return store[key] = value + '';
    });

    spyOn(localStorage, 'clear').andCallFake(function() {
      store = {};
    });

    spyOn(Object, 'keys').andCallFake(function(value) {
      var keys=[];

      for(var key in store) {
        keys.push(key);
      }

      return keys;
    });
  });

  // check to see if it has the expected function
  it('should have a get function', function() {
    expect(angular.isFunction(factory.get)).toBe(true);
    expect(angular.isFunction(factory.put)).toBe(true);
  });

  //check to see if it returns three notes initially
  it('should return three todo notes initially', function() {
    var result = factory.get();

    expect(result.length).toBe(3);
  });

  //check if it successfully adds a new item
  it('should return four todo notes after adding one more', function() {
    factory.put('Angular is awesome');

    var result = factory.get();
    expect(result.length).toBe(4);
  });
});

测试的过程和前面提到的TodoController的测试很类似。记住,实际上的factory使用localStorage来存储和提取笔记项目。但是,由于我们在进行单元测试于是我们并不想要依赖外部服务。因此,我们需要转换localStorage.getItem()以及localStorage.serItem()为假函数,而不是使用真正的localStorage函数。spuOn(localStorage,’setItem’).andCallFake()就是用来做这件事的。spyOn函数的第一个参数指明了我们感兴趣的对象,第二个参数指明了我们需要去监视的函数。andCallfake()让欧文们可以编写自己的函数。因此,在这个测试中我们已经完成了对于localStorage中使用的函数的配置。在我们的factory中我们也要使用Object.keys()函数来进行迭代以获取笔记的总数目。因此,在这个简单的例子中我们也能对Object.keys(localStorage)进行监视并返回我们的数组中的值,而不是localStorage。

接下来,我们检查了这个factory中是否包含我们需要的函数(get()和put())。这可以通过angular.isFunction()函数来完成。然后我们检查了这个factory中时候初始化了3个笔记。最后我们添加了一个新笔记然后断言笔记的数目添加了一个。

测试一个过滤器

现在,假设我们需要修改笔记在页面上展示的方式。如果一个笔记的字数超过了20个字符我们只应该展示前10个。我们现在就来编写一个简单的过滤器truncate来做这件事:

todoApp.filter('truncate', function() {
  return function(input,length) {
    return (input.length > length ? input.substring(0, length) : input );
  };
});

在页面标记中,它应该这样被使用:

{{note | truncate:20}}

为了对这个过滤器进行单元测试,我们应该编写下面的代码:

describe('filter tests', function() {
  beforeEach(module('todoApp'));
  it('should truncate the input to 10 characters',
    //this is how we inject a filter by appending Filter to the end of the filter name
    inject(function(truncateFilter) {
      expect(truncateFilter('abcdefghijkl', 10).length).toBe(10);
    })
  );
});

测试一个指令

我们来创建一个简单的指令,它可以给绑定这个指令的元素添加一个背景色。这可以通过CSS简单的完成。但是,为了来学习如何测试一个指令,我们还是编写了下面的代码:

todoApp.directive('customColor', function() {
  return {
    restrict: 'A',
    link: function(scope, elem, attrs) {
      elem.css({'background-color': attrs.customColor});
    }
  };
});

这个指令可以运用于任何元素,例如< span>ul custom-color=”rgb(128,128,128)”<< span>/ul<。测试代码如下所示:

describe('directive tests', function() {
    beforeEach(module('todoApp'));
  it('should set background to rgb(128, 128, 128)',
    inject(function($compile,$rootScope) {
      scope = $rootScope.$new();

      // get an element representation
      elem = angular.element("<span custom-color=\"rgb(128, 128, 128)\">sample</span>");

      // create a new child scope
      scope = $rootScope.$new();

      // finally compile the HTML
      $compile(elem)(scope);

      // expect the background-color css property to be desirabe one
      expect(elem.css("background-color")).toEqual('rgb(128, 128, 128)');
     })
  );
});

我们需要一个叫做$compile的服务来完成实际的编译,然后测试我们想要进行测试的元素。angular.element()会创建一个jqLite或者jQuery(如果可用)元素为我们所用。接着,我们将这个元素编译到一个作用域,它现在就可以被测试了。在上面的例子中我们希望background-color属性等于rgb(128,128,128)。

在Angular中使用E2E测试

在E2E测试中我们将一堆组件合起来然后检查全过程是否如我们预想的那样运运作。在我们的例子中我们想要确保当一个用户在文本输入框中输入文本然后点击按钮的时候,信息能够被添加到localStorage中然后出现在文本框下面的列表中。

E2E测试会使用一个Angular的场景运行器。如果你已经下载了demo应用并且解压完成,你可以看到test/e2e中有一个runner.html文件。这就是我们的场景运行文件。scenario.js文件包含了e2e测试(你应该在这里编写测试)。在编写完测试之后,你可以运行 http://localhost/angular-seed/test/e2e/runner.html 来查看结果。E2E测试的代码如下所示:

describe('my app', function() {
  beforeEach(function() {
    browser().navigateTo('../../app/notes.html');
  });

  var oldCount = -1;

  it("entering note and performing click", function() {
    element('ul').query(function($el, done) {
      oldCount = $el.children().length;
      done();
    });

    input('note').enter('test data');

    element('button').query(function($el, done) {
      $el.click();
      done();
    });
  });

  it('should add one more element now', function() {
    expect(repeater('ul li').count()).toBe(oldCount + 1);
  });        
});

解释

当我们要进行一次测试的时候我们应该首先导航到我们的主要HTML页面,app/notes.html。这可以通过browser.navigateTo()来完成。element.query()函数选择了ul元素并且记录了其中有哦多少个初始化项目。这个制备存储在oldCount变量中。接着,我们模拟在文本框中输入一个笔记,通过input(’note’)enter()完成。需要注意的是你需要将模型的名称传递给input函数。在我们的HTML页面中input被绑定到了ng-model=note。因此,模型名称应该被用来识别我们的输入字段。然后我们对按钮进行了一次点击然后检查列表中是否增加了一个新的笔记(li元素)。我们可以通过将新的笔记数目和旧的笔记数目进行对比得出结论。

总结

AngularJS应用的开发过程和测试似乎分不开的,尤其是TDD。一开始看似花费时间的测试一定会在最后帮你省下很多修复bug的时间。

本文译自Unit and End to End Testing in AngularJS,原文地址 http://www.sitepoint.com/unit-and-e2e-testing-in-angularjs/

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值