AngularJS разработан с учетом тестируемости. Внедрение зависимостей является одной из характерных особенностей инфраструктуры , облегчающей модульное тестирование. AngularJS определяет способ аккуратной модульности приложения и разделения его на различные компоненты, такие как контроллеры, директивы, фильтры или анимации. Эта модель разработки означает, что отдельные части работают изолированно, и приложение может легко масштабироваться в течение длительного периода времени. Поскольку расширяемость и тестируемость идут рука об руку, легко тестировать код AngularJS.
Согласно определению модульного тестирования , тестируемая система должна тестироваться изолированно. Таким образом, любые внешние объекты, необходимые системе, должны быть заменены фиктивными объектами. Как говорит само название, фиктивные объекты не выполняют реальной задачи; скорее они используются, чтобы оправдать ожидания тестируемой системы. Если вам нужно освежить знания о насмешках, обратитесь к одной из моих предыдущих статей: Зависимости от издевательств в тестах AngularJS .
В этой статье я поделюсь набором советов по тестированию сервисов, контроллеров и провайдеров в AngularJS. Фрагменты кода были написаны с использованием Jasmine и могут быть запущены с тестером Karma . Вы можете скачать код, использованный в этой статье, из нашего репозитория GitHub , где вы также найдете инструкции по запуску тестов.
Услуги тестирования
Сервисы являются одним из наиболее распространенных компонентов в приложении AngularJS. Они обеспечивают способ определения многократно используемой логики в центральном месте, так что нет необходимости повторять одну и ту же логику снова и снова. Синглтонный характер службы позволяет совместно использовать один и тот же фрагмент данных между несколькими контроллерами, директивами и даже другими службами.
Служба может зависеть от набора других служб для выполнения своей задачи. Скажем, служба с именем A зависит от служб B, C и D для выполнения своей задачи. При тестировании службы A зависимости B, C и D должны быть заменены на макеты.
Мы обычно высмеиваем все зависимости, кроме определенных служебных сервисов, таких как $rootScope
и $parse
. Мы создаем шпионов для методов, которые должны проверяться в тестах (в Jasmine jasmine.createSpy()
упоминаются как шпионы) с помощью jasmine.createSpy()
который возвращает совершенно новую функцию.
Давайте рассмотрим следующий сервис:
angular.module('services', []) .service('sampleSvc', ['$window', 'modalSvc', function($window, modalSvc){ this.showDialog = function(message, title){ if(title){ modalSvc.showModalDialog({ title: title, message: message }); } else { $window.alert(message); } }; }]);
Этот сервис имеет только один метод ( showDialog
). В зависимости от значения входных данных, которые получает этот метод, он вызывает одну из двух служб, которые внедряются в него как зависимости ( $window
или modalSvc
).
Чтобы протестировать sampleSvc
нам нужно sampleSvc
оба зависимых сервиса, загрузить угловой модуль, который содержит наш сервис, и получить ссылки на все объекты:
var mockWindow, mockModalSvc, sampleSvcObj; beforeEach(function(){ module(function($provide){ $provide.service('$window', function(){ this.alert= jasmine.createSpy('alert'); }); $provide.service('modalSvc', function(){ this.showModalDialog = jasmine.createSpy('showModalDialog'); }); }); module('services'); }); beforeEach(inject(function($window, modalSvc, sampleSvc){ mockWindow=$window; mockModalSvc=modalSvc; sampleSvcObj=sampleSvc; }));
Теперь мы можем проверить поведение метода showDialog
. Мы можем написать два тестовых примера для метода:
- он вызывает
alert
еслиtitle
не передан в параметре - он вызывает
showModalDialog
если присутствуют какtitle
и параметрыmessage
Следующий фрагмент показывает эти тесты:
it('should show alert when title is not passed into showDialog', function(){ var message="Some message"; sampleSvcObj.showDialog(message); expect(mockWindow.alert).toHaveBeenCalledWith(message); expect(mockModalSvc.showModalDialog).not.toHaveBeenCalled(); }); it('should show modal when title is passed into showDialog', function(){ var message="Some message"; var title="Some title"; sampleSvcObj.showDialog(message, title); expect(mockModalSvc.showModalDialog).toHaveBeenCalledWith({ message: message, title: title }); expect(mockWindow.alert).not.toHaveBeenCalled(); });
Этот метод не имеет большой логики для тестирования, тогда как сервисы в типичных веб-приложениях обычно содержат много функций. Вы можете использовать технику, показанную в этом совете, для насмешек и получения ссылок на сервисы. Сервисные тесты должны охватывать все возможные сценарии, которые предполагались при написании сервиса.
Фабрики и ценности также могут быть проверены с использованием той же методики.
Тестирование контроллеров
Процесс настройки для тестирования контроллера сильно отличается от процесса службы. Это связано с тем, что контроллеры не являются инъекционными, они создаются автоматически при загрузке маршрута или при компиляции директивы ng-controller
. Поскольку у нас нет загрузок представлений в тестах, нам нужно вручную создать экземпляр тестируемого контроллера.
Поскольку контроллеры обычно привязаны к представлению, поведение методов в контроллерах зависит от представлений. Кроме того, некоторые дополнительные объекты могут быть добавлены в область после компиляции представления. Одним из наиболее распространенных примеров этого является объект формы. Чтобы тесты работали должным образом, эти объекты необходимо создать вручную и добавить в контроллер.
Контроллер может быть одного из следующих типов:
- Контроллер используется с
$scope
- Контроллер используется с
Controller as
синтаксиса
Если вы не уверены в разнице, вы можете прочитать об этом здесь . В любом случае, мы обсудим оба этих случая.
Тестирование контроллеров с $ scope
Рассмотрим следующий контроллер:
angular.module('controllers',[]) .controller('FirstController', ['$scope','dataSvc', function($scope, dataSvc) { $scope.saveData = function () { dataSvc.save($scope.bookDetails).then(function (result) { $scope.bookDetails = {}; $scope.bookForm.$setPristine(); }); }; $scope.numberPattern = /^\d*$/; }]);
Чтобы протестировать этот контроллер, нам нужно создать экземпляр контроллера, передав объект $scope
и dataSvc
объект службы ( dataSvc
). Так как сервис содержит асинхронный метод, нам нужно смоделировать это, используя технику мошенничества, описанную в предыдущей статье.
Следующий фрагмент dataSvc
службу dataSvc
:
module(function($provide){ $provide.factory('dataSvc', ['$q', function($q) function save(data){ if(passPromise){ return $q.when(); } else { return $q.reject(); } } return{ save: save }; }]); });
Затем мы можем создать новую область видимости для контроллера, используя метод $rootScope.$new
. После создания экземпляра контроллера у нас есть все поля и методы в этой новой области $scope
.
beforeEach(inject(function($rootScope, $controller, dataSvc){ scope=$rootScope.$new(); mockDataSvc=dataSvc; spyOn(mockDataSvc,'save').andCallThrough(); firstController = $controller('FirstController', { $scope: scope, dataSvc: mockDataSvc }); }));
Поскольку контроллер добавляет поле и метод к $scope
, мы можем проверить, установлены ли для них правильные значения и правильная ли логика у методов. Приведенный выше пример контроллера добавляет регулярное выражение для проверки правильности числа. Давайте добавим спецификацию для проверки поведения регулярного выражения:
it('should have assigned right pattern to numberPattern', function(){ expect(scope.numberPattern).toBeDefined(); expect(scope.numberPattern.test("100")).toBe(true); expect(scope.numberPattern.test("100aa")).toBe(false); });
Если контроллер инициализирует какие-либо объекты со значениями по умолчанию, мы можем проверить их значения в спецификации.
Чтобы протестировать метод saveData
, нам нужно установить некоторые значения для объектов bookDetails
и bookForm
. Эти объекты будут связаны с элементами пользовательского интерфейса, поэтому создаются во время выполнения, когда представление компилируется. Как уже упоминалось, нам нужно вручную инициализировать их некоторыми значениями перед вызовом метода saveData
.
Следующий фрагмент тестирует этот метод:
it('should call save method on dataSvc on calling saveData', function(){ scope.bookDetails = { bookId: 1, name: "Mastering Web application development using AngularJS", author:"Peter and Pawel" }; scope.bookForm = { $setPristine: jasmine.createSpy('$setPristine') }; passPromise = true; scope.saveData(); scope.$digest(); expect(mockDataSvc.save).toHaveBeenCalled(); expect(scope.bookDetails).toEqual({}); expect(scope.bookForm.$setPristine).toHaveBeenCalled(); });
Тестирование контроллеров с синтаксисом «контроллер как»
Тестирование контроллера, использующего Controller as
синтаксиса, проще, чем тестирование с использованием $scope
. В этом случае экземпляр контроллера играет роль модели. Следовательно, все действия и объекты доступны в этом экземпляре.
Рассмотрим следующий контроллер:
angular.module('controllers',[]) .controller('SecondController', function(dataSvc){ var vm=this; vm.saveData = function () { dataSvc.save(vm.bookDetails).then(function(result) { vm.bookDetails = {}; vm.bookForm.$setPristine(); }); }; vm.numberPattern = /^\d*$/; });
Процесс вызова этого контроллера аналогичен процессу, рассмотренному ранее. Разница лишь в том, что нам не нужно создавать $scope
.
beforeEach(inject(function($controller){ secondController = $controller('SecondController', { dataSvc: mockDataSvc }); }));
Поскольку все члены и методы в контроллере добавляются к этому экземпляру, мы можем получить к ним доступ, используя ссылку на экземпляр.
Следующий фрагмент numberPattern
поле numberPattern
добавленное к вышеуказанному контроллеру:
it('should have set pattern to match numbers', function(){ expect(secondController.numberPattern).toBeDefined(); expect(secondController.numberPattern.test("100")).toBe(true); expect(secondController.numberPattern.test("100aa")).toBe(false); });
Утверждения метода saveData
остаются прежними. Единственное отличие в этом подходе заключается в том, как мы инициализируем значения для объектов bookDetails
и bookForm
.
Следующий фрагмент показывает спецификацию:
it('should call save method on dataSvc on calling saveData', function () secondController.bookDetails = { bookId: 1, name: "Mastering Web application development using AngularJS", author: "Peter and Pawel" }; secondController.bookForm = { $setPristine: jasmine.createSpy('$setPristine') }; passPromise = true; secondController.saveData(); rootScope.$digest(); expect(mockDataSvc.save).toHaveBeenCalled(); expect(secondController.bookDetails).toEqual({}); expect(secondController.bookForm.$setPristine).toHaveBeenCalled(); });
Провайдеры тестирования
Поставщики используются для предоставления API для конфигурации всего приложения, которую необходимо выполнить до запуска приложения. После завершения этапа настройки приложения AngularJS взаимодействие с поставщиками не допускается. Следовательно, провайдеры доступны только в блоках конфигурации или других блоках провайдеров. Мы не можем получить экземпляр провайдера, используя блок ввода, скорее нам нужно передать обратный вызов в блок модуля.
Давайте рассмотрим следующий поставщик, который зависит от константы ( appConstants
) второго поставщика ( anotherProvider
):
angular.module('providers', []) .provider('sample', function(appConstants, anotherProvider){ this.configureOptions = function(options){ if(options.allow){ anotherProvider.register(appConstants.ALLOW); } else { anotherProvider.register(appConstants.DENY); } }; this.$get = function(){}; });
Чтобы проверить это, нам сначала нужно смоделировать зависимости. Вы можете увидеть, как это сделать, в примере кода .
Перед тестированием провайдера мы должны убедиться, что модуль загружен и готов. В тестах загрузка модулей откладывается до тех пор, пока не будет выполнен блок ввода или не будет выполнен первый тест. В нескольких проектах я видел несколько тестов, которые используют пустой первый тест для загрузки модуля. Я не являюсь поклонником этого подхода, поскольку тест ничего не делает и добавляет счет к общему количеству тестов. Вместо этого я использую пустой блок ввода для загрузки модулей.
Следующий фрагмент получает ссылки и загружает модули:
beforeEach(module("providers")); beforeEach(function(){ module(function(anotherProvider, appConstants, sampleProvider){ anotherProviderObj=anotherProvider; appConstantsObj=appConstants; sampleProviderObj=sampleProvider; }); }); beforeEach(inject());
Теперь, когда у нас есть все ссылки, мы можем вызвать методы, определенные в поставщиках, и протестировать их:
it('should call register with allow', function(){ sampleProviderObj.configureOptions({allow:true}); expect(anotherProviderObj.register).toHaveBeenCalled(); expect(anotherProviderObj.register).toHaveBeenCalledWith(appConstantsObj.ALLOW); });
Вывод
Модульное тестирование иногда становится сложным, но на него стоит потратить время, поскольку оно обеспечивает правильность приложения. AngularJS облегчает модульное тестирование кода, написанного с использованием фреймворка. Я надеюсь, что эта статья даст вам достаточно идей для расширения и улучшения тестов в ваших приложениях. В следующей статье мы продолжим рассмотрение того, как тестировать другие части вашего кода.