Spec-Zone.ru › Angular.js 1.7

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

Язык JavaScript — это язык с динамической типизацией, который обладает большой выразительностью, но при этом практически не получает помощи от компилятора. По этой причине мы считаем, что любой код, написанный на JavaScript, должен сопровождаться набором тестов. Мы добавили в AngularJS множество функций, которые облегчают тестирование приложений 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, $rootScope;

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

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

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

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

Spec-Zone.ru

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