Статьи

Блок и сквозное тестирование в AngularJS

Модульное тестирование – это метод, который помогает разработчикам проверять отдельные фрагменты кода. Сквозное тестирование (E2E) вступает в игру, когда вы хотите убедиться, что набор компонентов, когда они объединены, работают как положено. AngularJS, являясь современной средой JavaScript MVC, предлагает полную поддержку модульных тестов и тестов E2E. Написание тестов при разработке приложений Angular может сэкономить вам много времени, которое вы бы потратили на исправление неожиданных ошибок. В этом руководстве объясняется, как включить модульные тесты и тесты E2E в приложение Angular. В этом руководстве предполагается, что вы знакомы с разработкой AngularJS. Вам также должны быть удобны различные компоненты, составляющие приложение Angular.

Мы будем использовать Jasmine в качестве основы для тестирования и Karma в качестве тестера. Вы можете использовать Yeoman, чтобы легко подготовить проект для вас, или просто быстро получить приложение углового семени от GitHub.

Если у вас нет среды тестирования, просто выполните следующие действия:

  1. Скачайте и установите Node.js , если у вас его еще нет.
  2. Установите Karma, используя npm ( npm install -g karma ).
  3. Загрузите демонстрационное приложение этого руководства с GitHub и разархивируйте его.

Внутри разархивированного приложения вы можете найти тесты в каталогах test/unit и test/e2e . Чтобы увидеть результат модульных тестов, просто запустите scripts/test.bat , который запускает сервер Karma. Наш основной HTML-файл – app/notes.html , доступ к нему можно получить по адресу http: //localhost/angular-seed/app/notes.html .

Начало работы с юнит-тестами

Вместо того, чтобы просто смотреть, как пишутся юнит-тесты, давайте создадим простое приложение на Angular и посмотрим, как юнит-тест вписывается в процесс разработки. Итак, давайте начнем с приложения и одновременно применим модульные тесты к различным компонентам. В этом разделе вы узнаете, как выполнить модульное тестирование:

  • Контроллеры
  • Директивы
  • фильтры
  • Фабрики

Мы собираемся создать очень простое приложение для ведения заметок. Наша разметка будет содержать текстовое поле, где пользователь может написать простую заметку. При нажатии кнопки заметка добавляется в список заметок. Мы будем использовать локальное хранилище HTML5 для хранения заметок. Начальная разметка HTML показана ниже. Bootstrap используется для быстрого построения макета.

 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html ng-app="todoApp"> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.2/angular.min.js" type="text/javascript"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js" type="text/javascript"></script> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" type="text/css"/> <script type="text/javascript" src="js/app.js"></script> <style> .center-grey{ background:#f2f2f2; margin-top:20; } .top-buffer { margin-top:20px; } button{ display: block; width: 100%; } </style> <title>Angular Todo Note App</title> </head> <body> <div class="container center-grey" ng-controller="TodoController"> <div class="row top-buffer" > <span class="col-md-3"></span> <span class="col-md-5"> <input class="form-control" type="text" ng-model="note" placeholder="Add a note here"/> </span> <span class="col-md-1"> <button ng-click="createNote()" class="btn btn-success">Add</button> </span> <span class="col-md-3"></span> </div> <div class="row top-buffer" > <span class="col-md-3"></span> <span class="col-md-6"> <ul class="list-group"> <li ng-repeat="note in notes track by $index" class="list-group-item"> <span>{{note}}</span> </li> </ul> </span> <span class="col-md-3"></span> </div> </div> </body> </html> 

Как видно из приведенной выше разметки, наш угловой модуль – todoApp а контроллер – TodoController . Входной текст привязан к модели note . Существует также список, который показывает все элементы заметки, которые были добавлены. Кроме того, когда кнопка нажата, TodoController createNote() нашего TodoController . Теперь давайте откроем включенный файл app.js и создадим модуль и контроллер. Добавьте следующий код в app.js

 var todoApp = angular.module('todoApp',[]); todoApp.controller('TodoController', function($scope, notesFactory) { $scope.notes = notesFactory.get(); $scope.createNote = function() { notesFactory.put($scope.note); $scope.note = ''; $scope.notes = notesFactory.get(); } }); todoApp.factory('notesFactory', function() { return { put: function(note) { localStorage.setItem('todo' + (Object.keys(localStorage).length + 1), note); }, get: function() { var notes = []; var keys = Object.keys(localStorage); for(var i = 0; i < keys.length; i++) { notes.push(localStorage.getItem(keys[i])); } return notes; } }; }); 

Наш TodoController использует фабрику notesFactory для хранения и извлечения заметок. Когда createNote() функция createNote() , она использует фабрику для помещения заметки в localStorage а затем очищает модель note . Таким образом, если бы мы проводили модульный тест TodoController нам нужно было бы убедиться, что при инициализации контроллера scope содержит определенное количество заметок. После запуска функции createNote() области количество заметок должно быть на единицу больше, чем при предыдущем подсчете. Код для нашего модульного теста показан ниже.

 describe('TodoController Test', function() { beforeEach(module('todoApp')); // will be run before each it() function // we don't need the real factory here. so, we will use a fake one. var mockService = { notes: ['note1', 'note2'], //just two elements initially get: function() { return this.notes; }, put: function(content) { this.notes.push(content); } }; // now the real thing: test spec it('should return notes array with two elements initially and then add one', inject(function($rootScope, $controller) { //injects the dependencies var scope = $rootScope.$new(); // while creating the controller we have to inject the dependencies too. var ctrl = $controller('TodoController', {$scope: scope, notesFactory:mockService}); // the initial count should be two expect(scope.notes.length).toBe(2); // enter a new note (Just like typing something into text box) scope.note = 'test3'; // now run the function that adds a new note (the result of hitting the button in HTML) scope.createNote(); // expect the count of notes to have been increased by one! expect(scope.notes.length).toBe(3); }) ); }); 

объяснение

Метод describe() определяет набор тестов. Он просто говорит, какие тесты включены в комплект. Внутри этого у нас есть beforeEach() которая выполняется непосредственно перед каждой функцией it() . Функция it() является нашей тестовой спецификацией и содержит фактический тест, который необходимо провести. Итак, перед выполнением каждого теста нам нужно загрузить наш модуль.

Поскольку это модульный тест, нам не нужны внешние зависимости. Вы уже знаете, что наш контроллер зависит от notesFactory для обработки заметок. Итак, для модульного тестирования контроллера нам нужно использовать фиктивную фабрику или сервис. Вот почему мы создали mockService , который просто имитирует реальную notesFactory и имеет те же функции, get() и put() . В то время как наша настоящая фабрика использует localStorage для хранения заметок, поддельная использует базовый массив.

Теперь давайте рассмотрим функцию it() которая используется для выполнения теста. Вы можете видеть, что он объявляет две зависимости $rootScope и $controller которые автоматически вводятся Angular. Эти две службы необходимы для получения корневой области приложения и создания контроллеров соответственно.

Служба $controller требует двух аргументов. Первым является имя контроллера для создания. Второй – это объект, представляющий зависимости контроллера. $rootScope.$new() возвращает новую $rootScope.$new() область, которая требуется нашему контроллеру. Обратите внимание, что мы также передали нашу поддельную фабричную реализацию контроллеру.

Теперь expect(scope.notes.length).toBe(2) что при инициализации контроллера scope.notes содержит ровно две заметки. Если в нем больше или меньше двух нот, этот тест не пройден. Аналогичным образом мы заполняем модель note новым элементом и запускаем createNote() которая должна добавить новую заметку. Теперь expect(scope.notes.length).toBe(3) проверки для этого. Поскольку в начале мы инициализировали наш массив двумя элементами, после запуска createNote() он должен иметь еще один (три элемента). Вы можете увидеть, какие тесты провалились / преуспели в карме.

Тестирование фабрики

Теперь мы хотим провести модульное тестирование фабрики, чтобы убедиться, что она работает как положено. Тестовый пример для notesFactory показан ниже.

 describe('notesFactory tests', function() { var factory; // excuted before each "it()" is run. beforeEach(function() { // load the module module('todoApp'); // inject your factory for testing inject(function(notesFactory) { factory = notesFactory; }); var store = { todo1: 'test1', todo2: 'test2', todo3: 'test3' }; spyOn(localStorage, 'getItem').andCallFake(function(key) { return store[key]; }); spyOn(localStorage, 'setItem').andCallFake(function(key, value) { return store[key] = value + ''; }); spyOn(localStorage, 'clear').andCallFake(function() { store = {}; }); spyOn(Object, 'keys').andCallFake(function(value) { var keys=[]; for(var key in store) { keys.push(key); } return keys; }); }); // check to see if it has the expected function it('should have a get function', function() { expect(angular.isFunction(factory.get)).toBe(true); expect(angular.isFunction(factory.put)).toBe(true); }); //check to see if it returns three notes initially it('should return three todo notes initially', function() { var result = factory.get(); expect(result.length).toBe(3); }); //check if it successfully adds a new item it('should return four todo notes after adding one more', function() { factory.put('Angular is awesome'); var result = factory.get(); expect(result.length).toBe(4); }); }); 

Процедура тестирования такая же, как для TodoController за исключением нескольких мест. Помните, что настоящая фабрика использует localStorage для хранения и извлечения элементов заметок. Но, поскольку мы проводим модульное тестирование, мы не хотим зависеть от внешних сервисов. Итак, нам нужно преобразовать вызовы функций, такие как localStorage.getItem() и localStorage.setItem() в поддельные, чтобы использовать наше собственное хранилище вместо использования основного хранилища данных localStorage . spyOn(localStorage, 'setItem').andCallFake() делает это. Первый аргумент spyOn() указывает интересующий объект, а второй аргумент обозначает функцию, за которой мы хотим следить. andCallFake() дает нам возможность написать нашу собственную реализацию функции. Итак, в этом тесте мы настроили функции localStorage для использования нашей пользовательской реализации. На нашей фабрике мы также используем Object.keys() для итерации и получения общего количества заметок. Таким образом, в этом простом случае мы также можем следить за Object.keys(localStorage) для возврата ключей из нашего собственного хранилища, а не из локального хранилища.

Затем мы проверяем, содержит ли фабрика необходимые функции ( get() и put() ). Это делается с помощью angular.isFunction() . Затем мы проверяем, есть ли на заводе три записи изначально В последнем тесте мы добавляем новую заметку и утверждаем, что она увеличила количество заметок на единицу.

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

Теперь, скажем, нам нужно изменить способ отображения заметок на странице. Если текст заметки содержит более 20 символов, мы должны показать только первые 10. Давайте напишем для этого простой фильтр и назовем его truncate как показано ниже.

 todoApp.filter('truncate', function() { return function(input,length) { return (input.length > length ? input.substring(0, length) : input ); }; }); 

В разметке это можно использовать так:

 {{note | truncate:20}} 

Для его модульного тестирования можно использовать следующий код.

 describe('filter tests', function() { beforeEach(module('todoApp')); it('should truncate the input to 10 characters', //this is how we inject a filter by appending Filter to the end of the filter name inject(function(truncateFilter) { expect(truncateFilter('abcdefghijkl', 10).length).toBe(10); }) ); }); 

Предыдущий код довольно прост. Просто отметьте, что вы вводите фильтр, добавляя Filter в конец фактического имени фильтра. Тогда вы можете позвонить как обычно.

Тестирование Директивы

Давайте просто создадим простую директиву, которая придает цвет фона элементу, к которому она применяется. Это можно сделать очень легко с помощью CSS. Но, просто чтобы продемонстрировать тестирование директив, давайте придерживаться следующего:

 todoApp.directive('customColor', function() { return { restrict: 'A', link: function(scope, elem, attrs) { elem.css({'background-color': attrs.customColor}); } }; }); 

Это может быть применено к любому элементу, например, <ul custom-color="rgb(128, 128, 128)"></ul> . Код теста показан ниже.

 describe('directive tests', function() { beforeEach(module('todoApp')); it('should set background to rgb(128, 128, 128)', inject(function($compile,$rootScope) { scope = $rootScope.$new(); // get an element representation elem = angular.element("<span custom-color=\"rgb(128, 128, 128)\">sample</span>"); // create a new child scope scope = $rootScope.$new(); // finally compile the HTML $compile(elem)(scope); // expect the background-color css property to be desirabe one expect(elem.css("background-color")).toEqual('rgb(128, 128, 128)'); }) ); }); 

Нам нужен сервис с именем $compile (внедренный Angular) для фактической компиляции и тестирования элемента, к которому применяется директива. angular.element() создает элемент jqLite или jQuery (если имеется), который мы можем использовать. Затем мы компилируем его с областью действия, и он готов к тестированию. В этом случае мы ожидаем, что CSS-свойство background-color будет rgb(128, 128, 128) . Обратитесь к этому документу, чтобы узнать, какие методы вы можете вызвать для element .

E2E тесты с угловым

В тестах E2E мы объединяем набор компонентов и проверяем, работает ли весь процесс, как ожидалось. В нашем случае нам нужно убедиться, что когда пользователь вводит что-то в текстовое поле и нажимает на кнопку, это добавляется в localStorage и появляется в списке под текстовым полем.

В этом тесте E2E используется угловой сценарий. Если вы скачали демонстрационное приложение и распаковали его, вы можете увидеть, что внутри test/e2e . Это наш файл сценария. Файл scenarios.js содержит тесты e2e (тесты вы напишете здесь). После написания тестов вы можете запустить http: //localhost/angular-seed/test/e2e/runner.html, чтобы увидеть результаты. Тест E2E, который будет добавлен в scenarios.js , показан ниже.

 describe('my app', function() { beforeEach(function() { browser().navigateTo('../../app/notes.html'); }); var oldCount = -1; it("entering note and performing click", function() { element('ul').query(function($el, done) { oldCount = $el.children().length; done(); }); input('note').enter('test data'); element('button').query(function($el, done) { $el.click(); done(); }); }); it('should add one more element now', function() { expect(repeater('ul li').count()).toBe(oldCount + 1); }); }); 

объяснение

Поскольку мы выполняем полный тест, мы должны сначала перейти на нашу главную страницу HTML, app/notes.html . Это достигается с помощью browser.navigateTo() . Функция element.query() выбирает элемент ul чтобы записать, сколько элементов заметки присутствует изначально. Это значение хранится в переменной oldCount . Далее мы моделируем ввод заметки в текстовое поле с помощью input('note').enter() . Просто отметьте, что вам нужно передать имя модели в функцию input() . На нашей HTML-странице ввод связан с note ng-model . Таким образом, это должно быть использовано для идентификации нашего поля ввода. Затем мы нажимаем на кнопку и проверяем, добавила ли она новую заметку (элемент li ) в список. Мы делаем это, сравнивая новый счетчик (полученный repeater('ul li').count() ) со старым счетчиком.

Вывод

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

Дополнительные ресурсы

  1. Если ваша служба / фабрика использует службу http для вызова удаленного API, вы можете вернуть из нее поддельные данные для модульного тестирования. Вот руководство для этого.
  2. Этот документ с сайта Angular содержит полезную информацию о модульном тестировании.
  3. Если вы начинаете новый проект Angular, рассмотрите возможность использования Protractor для испытаний E2E.