Spec-Zone.ru › Angular.js 1.5

Улучшить эту документациюТестирование модулей

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

Разделение забот

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

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

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

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

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

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

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

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

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

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

При тестировании приложений Angular есть определённые инструменты, которые следует использовать, что значительно упростит настройку и запуск тестов.

Karma

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

Karma — это приложение NodeJS, которое должно быть установлено через npm/yarn. Полные инструкции по установке доступны на сайте 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 во время выполнения тестов. В противном случае могут возникнуть проблемы, если структура каталогов тестов отличается от структуры каталогов приложения.

Тестирование промисов

При тестировании промисов важно знать, что разрешение промисов связано с циклом переваривания. Это означает, что функции обратного вызова then, catch и finally промиса вызываются только после выполнения цикла переваривания. В тестах вы можете запустить цикл переваривания, вызвав функцию $apply области видимости. Если у вас нет области видимости в тесте, вы можете внедрить $rootScope и вызвать $apply на нём. Также есть пример тестирования промисов в документации службы $q.

Использование beforeAll()

Фикстуры beforeAll() Jasmine и before() mocha часто полезны для совместного настройки тестов — чтобы сократить время выполнения тестов или просто сделать тесты более сфокусированными.

По умолчанию ngMock создаёт инжектор на каждый тест, чтобы ваши тесты не влияли друг на друга. Однако, если мы хотим использовать beforeAll(), ngMock должен создать инжектор перед запуском любых тестовых случаев и поделиться этим инжектором между всеми случаями для этого describe. Для этого служит module.sharedInjector(). При вызове внутри блока describe, один инжектор используется для всех фикстур и тестовых случаев, выполняемых в этом блоке.

В примере ниже мы тестируем сервис, который долго генерирует ответ. Чтобы избежать написания всех утверждений в одном тестовом случае, module.sharedInjector() и фикстура Jasmine beforeAll() используются для запуска сервиса только один раз. Затем все тестовые случаи делают утверждения о свойствах, добавленных в экземпляр сервиса.

describe("Deep Thought", function() {

  module.sharedInjector();

  beforeAll(module("UltimateQuestion"));

  beforeAll(inject(function(DeepThought) {
    expect(DeepThought.answer).toBeUndefined();
    DeepThought.generateAnswer();
  }));

  it("has calculated the answer correctly", inject(function(DeepThought) {
    // Because of sharedInjector, we have access to the instance of the DeepThought service
    // that was provided to the beforeAll() hook. Therefore we can test the generated answer
    expect(DeepThought.answer).toBe(42);
  }));

  it("has calculated the answer within the expected time", inject(function(DeepThought) {
    expect(DeepThought.runTimeMillennia).toBeLessThan(8000);
  }));

  it("has double checked the answer", inject(function(DeepThought) {
    expect(DeepThought.absolutelySureItIsTheRightAnswer).toBe(true);
  }));

});

Образец проекта

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

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

Spec-Zone.ru

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