Spec-Zone.ru › Angular.js 1.6

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Karma

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

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

Jasmine

Jasmine — фреймворк разработки на основе поведенческого сценария для JavaScript, который стал наиболее популярным выбором для тестирования приложений AngularJS. 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

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

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

Поскольку AngularJS разделяет логику от слоя представления, контроллеры легко тестируются. Давайте рассмотрим, как мы можем протестировать контроллер ниже, который предоставляет $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);
  });
});

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

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

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

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

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 промиса вызываются только после выполнения цикла переваривания. В тестах вы можете запустить цикл переваривания, вызвав функцию $apply области видимости. Если у вас нет области видимости в тесте, вы можете ввести $rootScope и вызвать $q на нем. В документации к сервису $q также есть пример тестирования промисов.

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

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

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

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

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–2018 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://code.angularjs.org/1.6.9/docs/guide/unit-testing

Spec-Zone.ru

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