В процессе создания и поставки полнофункционального программного обеспечения мы применяем несколько методов для проверки правильности и качества программного обеспечения. Модульное тестирование является одним из этих методов. Многие организации уделяют большое внимание модульному тестированию, поскольку оно снижает стоимость поиска и устранения потенциальных проблем приложения.
Когда мы начинаем разрабатывать приложения с сотнями тысяч строк JavaScript, мы не можем избежать тестирования кода. Некоторые разработчики JavaScript говорят, что тестирование JavaScript является еще более важным, поскольку поведение языка неизвестно до времени выполнения.
К счастью, AngularJS облегчает тестирование кода, написанного с использованием инфраструктуры, благодаря поддержке таких функций, как Dependency Injection (DI). В трех из моих прошлых статей я обсуждал несколько советов по макетированию , как тестировать контроллеры, сервисы и провайдеров и как тестировать директивы . Эта статья будет посвящена тестированию блоков Bootstrap приложения AngularJS (включая блоки конфигурации, блоки выполнения и блоки разрешения маршрута), событиям области действия и анимации.
Вы можете скачать код, использованный в этой статье, из нашего репозитория GitHub , где вы также найдете инструкции по запуску тестов.
Тестирование блоков Config и Run
Блоки конфигурации и запуска выполняются в начале жизненного цикла модуля. Они содержат важную логику, которая управляет работой модуля, виджета или приложения. Немного сложно проверить их, так как они не могут быть вызваны напрямую, как другие компоненты. В то же время их нельзя игнорировать, так как их роль имеет решающее значение.
Рассмотрим следующую конфигурацию и блоки выполнения:
angular.module('configAndRunBlocks', ['ngRoute']) .config(function ($routeProvider) { $routeProvider.when('/home', { templateUrl: 'home.html', controller: 'HomeController', resolve: { bootstrap: ['$q', function ($q) { return $q.when({ prop: 'value' }); }] } }) .when('/details/:id', { templateUrl: 'details.html', controller: 'DetailsController' }) .otherwise({ redirectTo: '/home' }); }) .run(function ($rootScope, messenger) { messenger.send('Bootstrapping application'); $rootScope.$on('$locationChangeStart', function (event, next, current) { messenger.send('Changing route to ' + next + ' from ' + current); }); });
Как и в случае тестирования поставщиков, мы должны убедиться, что модуль загружен, прежде чем тестировать функциональность внутри блоков конфигурации и запуска. Итак, мы будем использовать пустой блок ввода для загрузки модулей.
Следующий фрагмент проверяет зависимости, используемые в приведенном выше блоке, и загружает модуль:
describe('config and run blocks', function () { var routeProvider, messenger; beforeEach(function () { module('ngRoute'); module(function ($provide, $routeProvider) { routeProvider = $routeProvider; spyOn(routeProvider, 'when').andCallThrough(); spyOn(routeProvider, 'otherwise').andCallThrough(); messenger = { send: jasmine.createSpy('send') }; $provide.value('messenger', messenger); }); module('configAndRunBlocks'); }); beforeEach(inject()); });
Я намеренно не издевался $routeProvider
объектом $routeProvider
как мы протестируем зарегистрированные маршруты позже в этой статье.
Теперь, когда модуль загружен, блоки config и run уже выполнены. Итак, мы можем начать тестировать их поведение. Поскольку блок config регистрирует маршруты, мы можем проверить, правильно ли он зарегистрировал маршруты. Мы проверим, зарегистрировано ли ожидаемое количество маршрутов. Следующие тесты проверяют функциональность блока конфигурации:
describe('config block tests', function () { it('should have called registered 2 routes', function () { //Otherwise internally calls when. So, call count of when has to be 3 expect(routeProvider.when.callCount).toBe(3); }); it('should have registered a default route', function () { expect(routeProvider.otherwise).toHaveBeenCalled(); }); });
Блок выполнения в примере кода вызывает службу и регистрирует событие. Мы протестируем событие позже в этой статье. На данный момент давайте проверим вызов метода сервиса:
describe('run block tests', function () { var rootScope; beforeEach(inject(function ($rootScope) { rootScope = $rootScope; })); it('should send application bootstrap message', function () { expect(messenger.send).toHaveBeenCalled(); expect(messenger.send).toHaveBeenCalledWith("Bootstrapping application"); }); });
События области тестирования
Агрегация событий — один из хороших способов заставить два объекта взаимодействовать друг с другом, даже если они совершенно не знают друг друга. AngularJS предоставляет эту функцию через $broadcast
событий $emit
/ $broadcast
в $scope
. Любой объект в приложении может вызвать событие или прослушать событие в зависимости от необходимости.
При запуске приложения доступны как подписчики, так и издатели событий. Но поскольку модульные тесты написаны изолированно, у нас есть только один из объектов, доступных в модульных тестах. Таким образом, тестовая спецификация должна будет имитировать другой конец, чтобы иметь возможность проверить функциональность.
Давайте проверим событие, зарегистрированное в блоке выполнения выше:
$rootScope.$on('$locationChangeStart', function (event, next, current) { messenger.send('Changing route to ' + next + ' from ' + current); });
Событие $locationChangeStart
передается службой $location
каждый раз, когда изменяется местоположение приложения. Как уже упоминалось, нам нужно вручную запустить это событие и проверить, отправлено ли сообщение мессенджером. Следующий тест выполняет эту задачу:
it('should handle the $locationChangeStart event', function () { var next = '/second'; var current = '/first'; rootScope.$broadcast('$locationChangeStart', next, current); expect(messenger.send).toHaveBeenCalled(); expect(messenger.send).toHaveBeenCalledWith('Changing route to ' + next + ' from ' + current); });
Тестирование маршрутов
Маршруты определяют способ навигации по приложению. Любое неправильное или случайное изменение конфигурации маршрута приведет к ухудшению работы пользователя. Итак, у маршрутов тоже должны быть тесты.
На данный момент ngRoute и ui-router являются наиболее широко используемыми маршрутизаторами в приложениях AngularJS. Маршруты для обоих этих провайдеров должны быть определены в блоке конфигурации, а данные о маршрутах доступны через службы. Данные маршрута, настроенные с помощью ngRoute, доступны через сервис $route
. Данные маршрута ui-router доступны через сервис $state
. Эти сервисы могут использоваться для проверки правильности настройки правильного набора маршрутов.
Рассмотрим следующий блок конфигурации:
angular.module('configAndRunBlocks', ['ngRoute']) .config(function ($routeProvider) { $routeProvider.when('/home', { templateUrl: 'home.html', controller: 'HomeController', resolve: { bootstrap: ['$q', function ($q) { return $q.when({ prop: 'value' }); }] } }) .when('/details/:id', { templateUrl: 'details.html', controller: 'DetailsController' }) .otherwise({ redirectTo: '/home' }); });
Давайте теперь проверим эти маршруты. Для начала давайте возьмем ссылку на сервис $route
:
beforeEach(inject(function ($route) { route = $route; }));
Для маршрута /home
выше настроены templateUrl
, контроллер и блок разрешения. Давайте напишем утверждения, чтобы проверить их:
it('should have home route with right template, controller and a resolve block', function () { var homeRoute = route.routes['/home']; expect(homeRoute).toBeDefined(); expect(homeRoute.controller).toEqual('HomeController'); expect(homeRoute.templateUrl).toEqual('home.html'); expect(homeRoute.resolve.bootstrap).toBeDefined(); });
Тест на детали маршрута будет аналогичным. У нас также есть маршрут по умолчанию, настроенный с помощью блока иначе. Маршруты по умолчанию регистрируются с null
значением ключа. Вот тест для него:
it('should have a default route', function () { var defaultRoute = route.routes['null']; expect(defaultRoute).toBeDefined(); });
Тестирование блоков разрешения
Блоки разрешения — это фабрики, которые создаются при загрузке маршрута, и они доступны для контроллера, связанного с маршрутом. Это интересный сценарий для тестирования, поскольку его область действия ограничена маршрутом, и нам все еще нужно получить ссылку на объект.
Единственный способ проверить блок разрешения — это вызвать его с помощью службы $injector
. После запуска он может быть проверен как любая другая фабрика. В следующем фрагменте проверяется блок разрешения, настроенный с помощью домашнего маршрута, который мы создали выше:
it('should return data on calling the resolve block', function () { var homeRoute = route.routes['/home']; var bootstrapResolveBlock = homeRoute.resolve.bootstrap; httpBackend.expectGET('home.html').respond('<div>This is the homepage!</div>'); var bootstrapSvc = injector.invoke(bootstrapResolveBlock); //[1].call(q); bootstrapSvc.then(function (data) { expect(data).toEqual({ prop: 'value' }); }); rootScope.$digest(); httpBackend.flush(); });
Я должен был имитировать templateUrl
в вышеупомянутом тесте, поскольку AngularJS пытается перейти к маршруту по умолчанию, когда вызывается цикл дайджеста.
Тот же подход можно использовать и для тестирования $httpInterceptors
.
Тестирование анимации
Техника тестирования анимаций имеет некоторое сходство с директивами тестирования, но тестирование анимаций проще, поскольку анимации не так сложны, как директивы.
Библиотека angular- ngAnimateMock
содержит модуль ngAnimateMock
для упрощения тестирования анимаций. Этот модуль должен быть загружен перед тестированием анимации.
Рассмотрим следующую анимацию JavaScript:
angular.module('animationsApp', ['ngAnimate']).animation('.view-slide-in', function () { return { enter: function (element, done) { element.css({ opacity: 0.5, position: "relative", top: "10px", left: "20px" }) .animate({ top: 0, left: 0, opacity: 1 }, 500, done); }, leave: function (element, done) { element.animate({ opacity: 0.5, top: "10px", left: "20px" }, 100, done); } }; });
Давайте теперь напишем тесты, чтобы проверить правильность этой анимации. Нам нужно загрузить необходимые модули и получить ссылки на необходимые объекты.
beforeEach(function () { module('ngAnimate', 'ngAnimateMock', 'animationsApp'); inject(function ($animate, $rootScope, $rootElement) { $animate.enabled(true); animate = $animate; rootScope = $rootScope; rootElement = $rootElement; divElement = angular.element('<div class="view-slide-in">This is my view</div>'); rootScope.$digest(); }); });
Чтобы проверить rootElement
часть анимации, определенной выше, нам нужно программно заставить элемент ввести элемент rootElement
в приведенном выше фрагменте.
Перед тестированием анимации важно помнить, что AngularJS не позволяет анимациям запускаться до завершения первого цикла дайджеста. Это сделано для ускорения начальной привязки. Последнее утверждение в приведенном выше фрагменте запускает первый цикл дайджеста, поэтому нам не нужно делать это в каждом тесте.
Давайте проверим анимацию ввода, определенную выше. У него есть два теста:
- Элемент должен быть расположен на вершине 10 пикселей и оставлен на 20 пикселей с непрозрачностью 0,5 при входе
- Элемент должен быть расположен на вершине 0px и слева на 0px с непрозрачностью 1 через 1 секунду после входа. Это должен быть асинхронный тест, так как элемент управления должен будет ждать 1 секунду, прежде чем подтвердить
Ниже приведены тесты для двух вышеупомянутых случаев:
it('element should start entering from bottom right', function () { animate.enter(divElement, rootElement); rootScope.$digest(); expect(divElement.css('opacity')).toEqual('0.5'); expect(divElement.css('position')).toEqual('relative'); expect(divElement.css('top')).toEqual('10px'); expect(divElement.css('left')).toEqual('20px'); }); it('element should be positioned after 1 sec', function (done) { animate.enter(divElement, rootElement); rootScope.$digest(); setTimeout(function () { expect(divElement.css('opacity')).toEqual('1'); expect(divElement.css('position')).toEqual('relative'); expect(divElement.css('top')).toEqual('0px'); expect(divElement.css('left')).toEqual('0px'); done(); }, 1000); });
Аналогично, для анимации выхода нам нужно проверить значения свойств CSS через 100 мс. Поскольку тест должен ждать завершения анимации, нам нужно сделать тест асинхронным.
it('element should leave by sliding towards bottom right for 100ms', function (done) { rootElement.append(divElement); animate.leave(divElement, rootElement); rootScope.$digest(); setTimeout(function () { expect(divElement.css('opacity')).toEqual('0.5'); expect(divElement.css('top')).toEqual('10px'); expect(divElement.css('left')).toEqual('20px'); done(); }, 105); //5 ms delay in the above snippet is to include some time for the digest cycle });
Вывод
В этой статье я рассмотрел большинство советов по тестированию, которые я выучил за последние два года при тестировании кода AngularJS. Это не конец, и вы узнаете намного больше, когда будете писать тесты для бизнес-сценариев реальных приложений. Я надеюсь, что вы уже получили достаточно знаний по тестированию кода AngularJS. Зачем ждать? Просто идите и пишите тесты для каждой отдельной строки кода, которую вы написали до сих пор!