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

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, который устанавливает свойство в области видимости на основе длины пароля.

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() {

  var $filter;

  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 transclude-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.4.14/docs/guide/unit-testing

Spec-Zone.ru

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