AngularJS中的单元和端到端测试

单元测试是一种技术,可帮助开发人员验证隔离的代码段。 当您要确定一组组件集成在一起时,可以按预期工作时,就要进行端到端测试(E2E)。 AngularJS是现代JavaScript MVC框架,为单元测试和E2E测试提供全面支持。 在开发Angular应用程序时编写测试可以为您节省大量时间,否则您将浪费很多时间来修复意外的错误。 本教程将说明如何在Angular应用程序中合并单元测试和E2E测试。 本教程假定您熟悉AngularJS开发。 您还应该对构成Angular应用程序的不同组件感到满意。

我们将使用Jasmine作为测试框架,并使用Karma作为测试运行程序。 您可以使用Yeoman轻松地为您搭建项目,也可以从GitHub快速获取有角的种子应用程序

如果您没有测试环境,请按照以下步骤操作:

  1. 下载并安装Node.js (如果尚未安装)。
  2. 使用npm( npm install -g karmanpm install -g karma
  3. 从GitHub下载本教程的演示应用程序并将其解压缩。

在解压缩的应用程序内,您可以在test/unittest/e2e目录中找到测试。 要查看单元测试的结果,只需运行scripts/test.bat启动Karma服务器。 我们的主要HTML文件是app/notes.html ,可以从http://localhost/angular-seed/app/notes.html进行访问。

单元测试入门

让我们构建一个简单的Angular应用程序,看看单元测试如何适合开发过程,而不是仅仅看单元测试的编写方式。 因此,让我们从一个应用程序开始,然后将单元测试同时应用于各个组件。 在本节中,您将学习如何进行单元测试:

  • 控制器
  • 指令
  • 筛选器
  • 工厂工厂

我们将构建一个非常简单的待办事项笔记应用程序。 我们的标记将包含一个文本字段,用户可以在其中编写简单的注释。 当按下按钮时,便笺将添加到便笺列表中。 我们将使用HTML5 本地存储来存储注释。 初始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模型。 还有一个列表,显示所有已添加的注释项目。 此外,当单击按钮时,我们的TodoControllercreateNote()函数将运行。 现在,让我们打开包含的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的工厂来存储和检索注释。 运行createNote()函数时,它将使用工厂将注释放入localStorage ,然后清除note模型。 因此,如果要对TodoController进行单元测试, TodoController需要确保在初始化控制器时, 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()方法定义测试套件。 它只是说明套件中包含哪些测试。 里面有一个beforeEach()函数, it()在每个it()函数运行之前执行。 it()函数是我们的测试规格,并具有要进行的实际测试。 因此,在执行每个测试之前,我们需要加载模块。

由于这是一个单元测试,因此我们不需要外部依赖项。 您已经知道我们的控制器依靠notesFactory处理注释。 因此,要对控制器进行单元测试,我们需要使用模拟工厂或服务。 这就是为什么我们创建了mockServicemockService模拟了真正的notesFactory并具有相同的功能get()put() 。 虽然我们的实际工厂使用localStorage来存储笔记,但假工厂使用的是基础数组。

现在让我们检查用于执行测试的it()函数。 您会看到它声明了两个依赖项$rootScope$controller ,它们由Angular自动注入。 这两个服务分别是获取应用程序的根范围和创建控制器所必需的。

$controller服务需要两个参数。 第一个是要创建的控制器的名称。 第二个对象代表控制器的依赖关系。 $rootScope.$new()返回控制器所需要的新子范围。 注意,我们还将伪造的工厂实现传递给了控制器。

现在, expect(scope.notes.length).toBe(2)断言,控制器初始化时scope.notes包含两个注释。 如果它的注释超过或少于两个,则此测试将失败。 同样,我们用新项目填充note模型,然后运行应该添加新便笺的createNote()函数。 现在expect(scope.notes.length).toBe(3)对此进行检查。 因为从一开始我们就用两个项目初始化了数组,所以在运行createNote()它应该再有一个(三个项目)。 您可以查看在Karma中哪些测试失败/成功。

测试工厂

现在我们要对工厂进行单元测试,以确保它能按预期工作。 notesFactory如下所示。

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相同。 请记住,实际工厂使用localStorage来存储和检索注释项。 但是,由于我们正在进行单元测试,因此我们不想依赖外部服务。 因此,我们需要将诸如localStorage.getItem()localStorage.setItem()类的函数调用转换为伪函数,以使用我们自己的存储,而不是使用localStorage的基础数据存储。 spyOn(localStorage, 'setItem').andCallFake()执行此操作。 spyOn()的第一个参数指定感兴趣的对象,第二个参数表示我们要监视的功能。 andCallFake()提供了一种编写我们自己的函数实现的方法。 因此,在此测试中,我们将localStorage函数配置为使用我们的自定义实现。 在我们的工厂中,我们还使用Object.keys()函数进行迭代并获取注释的总数。 因此,在这种简单情况下,我们还可以监视Object.keys(localStorage)以从我们自己的存储而不是本地存储返回键。

接下来,我们检查工厂是否包含必需的函数( get()put() )。 这是通过angular.isFunction() 。 然后,我们检查工厂最初是否有三张纸条。 在上一个测试中,我们添加了一个新音符并断言它使音符数增加了一个。

测试过滤器

现在,假设我们需要修改注释在页面上的显示方式。 如果注释的文本超过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);
    })
  );
});

先前的代码非常简单。 只需注意,您可以通过将Filter附加到实际过滤器名称的末尾来注入过滤器。 然后您可以像往常一样调用它。

测试指令

让我们创建一个简单的指令,为它所应用的元素提供背景色。 使用CSS可以很容易地做到这一点。 但是,为了演示指令的测试,我们坚持以下几点:

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

这可以应用于任何元素,例如<ul custom-color="rgb(128, 128, 128)"></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注入)的服务来实际编译和测试在其上应用了指令的元素。 angular.element()创建一个jqLit​​e或jQuery(如果可用)元素供我们使用。 然后,我们用一个作用域对其进行编译,并准备对其进行测试。 在这种情况下,我们希望background-color CSS属性为rgb(128, 128, 128) 。 请参阅此文档 ,了解可以在element调用哪些方法。

端到端的E2E测试

在端到端测试中,我们将一组组件组合在一起,并检查整个过程是否按预期工作。 在我们的案例中,我们需要确保当用户在文本字段中输入内容并单击按钮时,它将添加到localStorage并出现在文本字段下方的列表中。

此E2E测试使用Angular场景运行器。 如果您下载了演示应用程序并将其解压缩,则可以看到test/e2e里面有一个runner.html 。 这是我们的方案运行器文件。 该scenarios.js文件包含端到端测试(你将在这里写的测试)。 编写测试后,您可以运行http://localhost/angular-seed/test/e2e/runner.html查看结果。 该E2E测试要添加到scenarios.js如下所示。

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('note').enter() 。 请注意,您需要将模型名称传递给input()函数。 在我们的HTML页面中,输入绑定到ng-model note 。 因此,应将其用于标识我们的输入字段。 然后,我们单击按钮,并检查是否在列表中添加了新注释( li元素)。 我们通过将新计数(由repeater('ul li').count() )与旧计数进行比较来实现。

结论

AngularJS在设计时就考虑到了可靠的JavaScript测试,并且偏向于测试驱动开发。 因此,在开发过程中请始终测试代码。 这似乎很耗时,但实际上它消除了以后会出现的大多数错误,从而节省了您的时间。

其他资源

  1. 如果您的服务/工厂使用http服务来调用远程API,则可以从中返回假数据以进行单元测试。 这是一个指南
  2. 这个来自Angular网站的文档提供了一些有关单元测试的良好信息。
  3. 如果要开始新的Angular项目,请考虑使用Protractor进行E2E测试。

From: https://www.sitepoint.com/unit-and-e2e-testing-in-angularjs/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值