Статьи

Модульное тестирование в AngularJS: сервисы, контроллеры и провайдеры

angularjs

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 облегчает модульное тестирование кода, написанного с использованием фреймворка. Я надеюсь, что эта статья даст вам достаточно идей для расширения и улучшения тестов в ваших приложениях. В следующей статье мы продолжим рассмотрение того, как тестировать другие части вашего кода.