Spec-Zone.ru › Angular.js 1.3
Улучшить эту документацию

JavaScript — язык с динамической типизацией, обладающий большой выразительностью, но при этом практически не получающий помощи от компилятора. По этой причине мы считаем, что любой код на JavaScript должен сопровождаться набором тестов. Мы интегрировали в Angular много функций, которые облегчают тестирование приложений Angular. Поэтому нет оправдания отсутствию тестирования.

Разделение обязанностей

Тестирование отдельных единиц кода, как следует из названия, называется модульным тестированием. Модульные тесты стремятся ответить на вопросы, такие как: «Правильно ли я реализовал логику?» или «Функция сортировки правильно упорядочивает список?»

Для ответа на такие вопросы очень важно изолировать тестируемую единицу кода. Ведь при тестировании функции сортировки мы не должны создавать связанные части, такие как элементы DOM, или выполнять какие-либо вызовы XHR для получения данных, подлежащих сортировке.

Хотя это может показаться очевидным, на типичном проекте это может быть очень сложно. Причина заключается в том, что разработчики часто смешивают обязанности, в результате чего получается код, выполняющий всё. Он отправляет запрос XHR, сортирует полученные данные и затем манипулирует DOM.

В Angular мы стараемся сделать правильные вещи лёгкими, поэтому мы предоставляем инъекцию зависимостей для ваших запросов XHR, которые можно подменить, и абстракции, позволяющие тестировать вашу модель без необходимости манипулирования DOM. Тест может утверждать, что данные отсортированы, не создавая и не рассматривая состояние DOM и не ожидая возврата данных от запросов XHR. Функция сортировки может тестироваться изолированно.

С великой силой приходит великая ответственность

Angular написан с учётом возможности тестирования, но всё же требует от вас выполнения правильных действий. Мы постарались сделать правильные действия лёгкими, но если вы игнорируете эти рекомендации, вы можете получить приложение, которое невозможно протестировать.

Инъекция зависимостей

В Angular встроенная инъекция зависимостей, что значительно упрощает тестирование компонентов, поскольку вы можете передавать зависимости компонента и подменять или имитировать их по своему усмотрению.

Компоненты, у которых инжектированы зависимости, легко подменяются в каждом тесте, без необходимости работы с глобальными переменными, которые могут непреднамеренно повлиять на другой тест.

Дополнительные инструменты для тестирования приложений Angular

Для тестирования приложений Angular существуют определённые инструменты, которые значительно облегчают их настройку и выполнение.

Karma

Karma — инструмент командной строки JavaScript, который может использоваться для запуска веб-сервера, загружающего исходный код приложения и выполняющего тесты. Вы можете настроить Karma для запуска в нескольких браузерах, что полезно для уверенности в том, что ваше приложение работает во всех необходимых браузерах. Karma выполняется в командной строке и отображает результаты ваших тестов в командной строке после их выполнения в браузере.

Karma — приложение NodeJS и должно устанавливаться через npm. Полные инструкции по установке доступны на сайте Karma.

Jasmine

Jasmine — фреймворк разработки на основе поведенческого подхода для JavaScript, который стал наиболее популярным выбором для тестирования приложений Angular. Jasmine предоставляет функции для структурирования тестов и создания утверждений. По мере роста тестов крайне важно поддерживать их структуру и документирование, и Jasmine помогает в этом.

В Jasmine мы используем функцию describe для группирования тестов:

describe("sorting the list of users", function() {
  // individual tests go here
});

А каждый отдельный тест определяется внутри вызова функции it:

describe('sorting the list of users', function() {
  it('sorts in descending order by default', function() {
    // your test assertion goes here
  });
});

Группирование связанных тестов в блоках describe и описание каждого отдельного теста в вызове it делает ваши тесты самодокументирующимися.

Наконец, Jasmine предоставляет утверждения, позволяющие делать проверки:

describe('sorting the list of users', function() {
  it('sorts in descending order by default', function() {
    var users = ['jack', 'igor', 'jeff'];
    var sorted = sortUsers(users);
    expect(sorted).toEqual(['jeff', 'jack', 'igor']);
  });
});

Jasmine поставляется с рядом утверждений, которые помогают выполнить различные проверки. Вы должны прочитать документацию Jasmine, чтобы узнать, что они собой представляют. Для использования Jasmine с Karma мы используем karma-jasmine.

angular-mocks

Angular также предоставляет модуль ngMock, который предоставляет возможности имитации для ваших тестов. Он используется для инъекции и имитации служб Angular в модульных тестах. Кроме того, он может расширять другие модули, делая их синхронными. Синхронность тестов делает их более чистыми и простыми в работе. Одна из наиболее полезных частей ngMock — $httpBackend, которая позволяет нам имитировать запросы XHR в тестах и возвращать образцы данных вместо реальных.

Тестирование контроллера

Поскольку Angular отделяет логику от слоя представления, контроллеры легко тестируются. Давайте посмотрим, как мы можем протестировать контроллер ниже, который предоставляет $scope.grade, устанавливая свойство в области видимости (scope) на основе длины пароля.

angular.module('app', [])
.controller('PasswordController', function PasswordController($scope) {
  $scope.password = '';
  $scope.grade = function() {
    var size = $scope.password.length;
    if (size > 8) {
      $scope.strength = 'strong';
    } else if (size > 3) {
      $scope.strength = 'medium';
    } else {
      $scope.strength = 'weak';
    }
  };
});

Поскольку контроллеры недоступны в глобальной области видимости, нам необходимо использовать angular.mock.inject для инъекции нашего контроллера. Первым шагом является использование функции module, предоставляемой angular-mocks. Она загружает заданный модуль, делая его доступным в ваших тестах. Мы передаём его в beforeEach, которая является функцией Jasmine, позволяющей выполнять код перед каждым тестом. Затем мы можем использовать inject для доступа к $controller, службе, отвечающей за создание контроллеров.

describe('PasswordController', function() {
  beforeEach(module('app'));

  var $controller;

  beforeEach(inject(function(_$controller_){
    // The injector unwraps the underscores (_) from around the parameter names when matching
    $controller = _$controller_;
  }));

  describe('$scope.grade', function() {
    it('sets the strength to "strong" if the password length is >8 chars', function() {
      var $scope = {};
      var controller = $controller('PasswordController', { $scope: $scope });
      $scope.password = 'longerthaneightchars';
      $scope.grade();
      expect($scope.strength).toEqual('strong');
    });
  });
});

Обратите внимание, как вложенные вызовы describe и описательные вызовы со строками делают тест очень понятным. Он точно документирует, что тестируется, и с первого взгляда вы быстро поймёте, что происходит. Теперь добавим тест для случая, когда длина пароля меньше трёх символов, в результате чего $scope.strength должно быть установлено в «weak»:

describe('PasswordController', function() {
  beforeEach(module('app'));

  var $controller;

  beforeEach(inject(function(_$controller_){
    // The injector unwraps the underscores (_) from around the parameter names when matching
    $controller = _$controller_;
  }));

  describe('$scope.grade', function() {
    it('sets the strength to "strong" if the password length is >8 chars', function() {
      var $scope = {};
      var controller = $controller('PasswordController', { $scope: $scope });
      $scope.password = 'longerthaneightchars';
      $scope.grade();
      expect($scope.strength).toEqual('strong');
    });

    it('sets the strength to "weak" if the password length <3 chars', function() {
      var $scope = {};
      var controller = $controller('PasswordController', { $scope: $scope });
      $scope.password = 'a';
      $scope.grade();
      expect($scope.strength).toEqual('weak');
    });
  });
});

Теперь у нас есть два теста, но обратите внимание на дублирование между ними. Оба должны создать переменную $scope и создать контроллер. По мере добавления новых тестов это дублирование будет только ухудшаться. К счастью, Jasmine предоставляет beforeEach, что позволяет нам запускать функцию перед каждым отдельным тестом. Посмотрим, как это упростит наши тесты:

describe('PasswordController', function() {
  beforeEach(module('app'));

  var $controller;

  beforeEach(inject(function(_$controller_){
    // The injector unwraps the underscores (_) from around the parameter names when matching
    $controller = _$controller_;
  }));

  describe('$scope.grade', function() {
    var $scope, controller;

    beforeEach(function() {
      $scope = {};
      controller = $controller('PasswordController', { $scope: $scope });
    });

    it('sets the strength to "strong" if the password length is >8 chars', function() {
      $scope.password = 'longerthaneightchars';
      $scope.grade();
      expect($scope.strength).toEqual('strong');
    });

    it('sets the strength to "weak" if the password length <3 chars', function() {
      $scope.password = 'a';
      $scope.grade();
      expect($scope.strength).toEqual('weak');
    });
  });
});

Мы вынесли дублирование в блок beforeEach. Каждый отдельный тест теперь содержит только код, специфичный для этого теста, а не код, общий для всех тестов. Расширяя свои тесты, следите за местами, где можно использовать beforeEach для их упорядочивания. beforeEach — не единственная функция такого рода, предоставляемая Jasmine, и в документации перечислены другие.

Тестирование фильтров

Фильтры — это функции, преобразующие данные в удобочитаемый для пользователя формат. Они важны, потому что снимают с логики приложения ответственность за форматирование, что ещё больше упрощает логику приложения.

myModule.filter('length', function() {
  return function(text) {
    return ('' + (text || '')).length;
  }
});

describe('length filter', function() {

  beforeEach(inject(function(_$filter_){
    $filter= _$filter_;
  }));

  it('returns 0 when given null', function() {
    var length = $filter('length');
    expect(length(null)).toEqual(0);
  });

  it('returns the correct value when given a string of chars', function() {
    var length = $filter('length');
    expect(length('abc')).toEqual(3);
  });
});

Тестирование директив

Директивы в Angular отвечают за инкапсуляцию сложной функциональности внутри пользовательских HTML-тегов, атрибутов, классов или комментариев. Модульные тесты очень важны для директив, потому что компоненты, созданные с помощью директив, могут использоваться во всём приложении и в самых разных контекстах.

Директива простого HTML-элемента

Давайте начнём с приложения Angular без зависимостей.

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

Теперь мы можем добавить директиву в наше приложение.

app.directive('aGreatEye', function () {
    return {
        restrict: 'E',
        replace: true,
        template: '<h1>lidless, wreathed in flame, {{1 + 1}} times</h1>'
    };
});

Эта директива используется как тег <a-great-eye></a-great-eye>. Она заменяет весь тег шаблоном <h1>lidless, wreathed in flame, {{1 + 1}} times</h1>. Теперь мы напишем модульный тест Jasmine для проверки этой функциональности. Обратите внимание, что выражение {{1 + 1}} также будет вычислено в рендеренном содержимом.

describe('Unit testing great quotes', function() {
  var $compile,
      $rootScope;

  // Load the myApp module, which contains the directive
  beforeEach(module('myApp'));

  // Store references to $rootScope and $compile
  // so they are available to all tests in this describe block
  beforeEach(inject(function(_$compile_, _$rootScope_){
    // The injector unwraps the underscores (_) from around the parameter names when matching
    $compile = _$compile_;
    $rootScope = _$rootScope_;
  }));

  it('Replaces the element with the appropriate content', function() {
    // Compile a piece of HTML containing the directive
    var element = $compile("<a-great-eye></a-great-eye>")($rootScope);
    // fire all the watches, so the scope expression {{1 + 1}} will be evaluated
    $rootScope.$digest();
    // Check that the compiled element contains the templated content
    expect(element.html()).toContain("lidless, wreathed in flame, 2 times");
  });
});

Мы инжектируем службу $compile и $rootScope перед каждым тестом Jasmine. Служба $compile используется для рендеринга директивы aGreatEye. После рендеринга директивы мы убеждаемся, что директива заменила содержимое и «lidless, wreathed in flame, 2 times» присутствует.

Использование нижнего подчеркивания: Использование обозначения с нижним подчеркиванием (например: _$rootScope_) — это распространённая в сообществе AngularJS конвенция для поддержания чистоты имён переменных в ваших тестах. Именно поэтому $injector удаляет ведущие и хвостовые нижние подчеркивания при сопоставлении параметров. Правило с нижним подчеркиванием применяется **только** тогда, когда имя начинается **и** заканчивается ровно одним нижним подчеркиванием, в противном случае замена не происходит.

Тестирование директив с трансклюзией

Директивы, использующие трансклюзию, обрабатываются компилятором особым образом. Перед вызовом их функции компиляции содержимое элемента директивы удаляется из элемента и предоставляется через функцию трансклюзии. Затем шаблон директивы добавляется к элементу директивы, в который затем можно вставить трансклюдированное содержимое в шаблон.

До компиляции:

<div translude-directive>
  Some transcluded content
</div>

После извлечения трансклюзии:

<div transclude-directive></div>

После компиляции:

<div transclude-directive>
  Some Template
  <span ng-transclude>Some transcluded content</span>
</div>

Если директива использует трансклюзию «element», компилятор фактически удалит весь элемент директивы из DOM и заменит его узлом комментария. Затем компилятор вставит шаблон директивы «после» этого узла комментария, как соседнего.

До компиляции

<div element-transclude>
  Some Content
</div>

После извлечения трансклюзии

<!-- elementTransclude -->

После компиляции:

<!-- elementTransclude -->
<div element-transclude>
  Some Template
  <span ng-transclude>Some transcluded content</span>
</div>

Важно помнить об этом при написании тестов для директив, использующих трансклюзию «element». Если вы поместите директиву на корневой элемент фрагмента DOM, который вы передаёте в $compile, то узел DOM, возвращаемый из функции связывания, будет узлом комментария, и вы потеряете возможность доступа к шаблону и трансклюдированному содержимому.

var node = $compile('<div element-transclude></div>')($rootScope);
expect(node[0].nodeType).toEqual(node.COMMENT_NODE);
expect(node[1]).toBeUndefined();

Для решения этой проблемы необходимо гарантировать, что ваша директива трансклюзии «element» находится внутри элемента, такого как <div>.

var node = $compile('<div><div element-transclude></div></div>')($rootScope);
var contents = node.contents();
expect(contents[0].nodeType).toEqual(node.COMMENT_NODE);
expect(contents[1].nodeType).toEqual(node.ELEMENT_NODE);

Тестирование директив с внешними шаблонами

Если ваша директива использует templateUrl, рассмотрите использование karma-ng-html2js-preprocessor, чтобы предварительно скомпилировать HTML-шаблоны и избежать необходимости загружать их через HTTP во время выполнения тестов. В противном случае могут возникнуть проблемы, если структура каталогов тестов отличается от структуры каталогов приложения.

Пример проекта

Посмотрите проект angular-seed для примера.

© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://code.angularjs.org/1.3.20/docs/guide/unit-testing

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API