Статьи

Зависимости от насмешек в тестах AngularJS

AngularJS был разработан с учетом тестирования. Исходный код фреймворка протестирован очень хорошо, и любой код, написанный с использованием фреймворка, также тестируем. Встроенный механизм внедрения зависимостей делает каждый компонент, написанный на AngularJS, тестируемым. Код в приложении AngularJS можно тестировать модульно, используя любую среду тестирования JavaScript. Наиболее широко используемый фреймворк для тестирования кода AngularJS — это Jasmine. Все примеры фрагментов в этой статье написаны с использованием Jasmine. Если вы используете любой другой тестовый фреймворк в своем проекте Angular, вы все равно можете применить идеи, обсуждаемые в этой статье.

В этой статье предполагается, что у вас уже есть некоторый опыт модульного тестирования и тестирования кода AngularJS. Вам не нужно быть экспертом в тестировании. Если у вас есть базовые знания о тестировании и вы можете написать несколько простых тестовых примеров для приложения AngularJS, вы можете продолжить чтение статьи.

Роль издевательства в юнит-тестах

Работа каждого модульного теста заключается в тестировании функциональности фрагмента кода в отдельности. Изоляция тестируемой системы может порой быть сложной, поскольку зависимости могут исходить из разных наборов источников, и нам необходимо полностью понимать обязанности объекта, который необходимо смоделировать.

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

Насмешка в тестах AngularJS

Поскольку одной из основных целей AngularJS является тестируемость, основная команда прошла лишнюю милю, чтобы упростить тестирование, и предоставила нам набор макетов в модуле angular-mocks. Этот модуль состоит из макетов вокруг набора сервисов AngularJS (то есть $ http, $ timeout, $ animate и т. Д.), Которые широко используются в любом приложении AngularJS. Этот модуль сокращает время, затрачиваемое разработчиками на написание тестов.

При написании тестов для реальных бизнес-приложений эти макеты очень помогают. В то же время их недостаточно для тестирования всего приложения. Нам нужно смоделировать любую зависимость, которая есть в фреймворке, но не смоделирована — зависимость, полученная от стороннего плагина, глобального объекта или зависимости, созданной в приложении. В этой статье будут рассмотрены некоторые советы по насмешке зависимостей AngularJS.

Сервисы издевательства

Сервис является наиболее распространенным типом зависимости в приложениях AngularJS. Как вы уже знаете, сервис является перегруженным термином в AngularJS. Это может относиться к услуге, фабрике, значению, константе или поставщику. Мы обсудим провайдеров в следующем разделе. Сервис может быть подделан одним из следующих способов:

  • Получение экземпляра реального сервиса с использованием блока инъекции и шпионских методов сервиса.
  • Реализация фиктивного сервиса с использованием $ provide.

Я не фанат первого подхода, так как он может привести к фактической реализации методов обслуживания. Мы будем использовать второй подход, чтобы издеваться над следующим сервисом:

angular.module('sampleServices', [])
  .service('util', function() {
    this.isNumber = function(num) {
      return !isNaN(num);
    };
         
    this.isDate = function(date) {
      return (date instanceof Date);
    };
  });

Следующий фрагмент кода создает макет вышеуказанного сервиса:

 module(function($provide) {
  $provide.service('util', function() {
    this.isNumber = jasmine.createSpy('isNumber').andCallFake(function(num) {
      //a fake implementation
    });
    this.isDate = jasmine.createSpy('isDate').andCallFake(function(num) {
      //a fake implementation
    });
  });
});

//Getting reference of the mocked service
var mockUtilSvc;

inject(function(util) {
  mockUtilSvc = util;
});

Хотя приведенный выше пример использует Jasmine для создания шпионов, вы можете заменить его эквивалентной реализацией, используя Sinon.js.

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

Константы, фабрики и значения могут быть смоделированы с использованием $ provide.constant , $ provide.factory и $ provide.value соответственно.

Издеватели

Насмешливые провайдеры похожи на издевательские услуги Все правила, которым нужно следовать при написании провайдеров, должны также соблюдаться при насмешках над ними. Рассмотрим следующий поставщик:

 angular.module('mockingProviders',[])
  .provider('sample', function() {
    var registeredVals = [];

    this.register = function(val) {
      registeredVals.push(val);      
    };

    this.$get = function() {
      function getRegisteredVals() {
        return registeredVals;
      }

      return {
        getRegisteredVals: getRegisteredVals
      };
    };
  });

Следующий фрагмент создает макет для вышеуказанного провайдера:

 module(function($provide) {
  $provide.provider('sample', function() {
    this.register = jasmine.createSpy('register');

    this.$get = function() {
      var getRegisteredVals = jasmine.createSpy('getRegisteredVals');

      return {
        getRegisteredVals: getRegisteredVals
      };
    };
  });
});

//Getting reference of the provider
var sampleProviderObj;

module(function(sampleProvider) {
  sampleProviderObj = sampleProvider;
});

Разница между получением ссылки на провайдеров и другими синглетонами заключается в том, что провайдеры недоступны в inject () блокировке, поскольку к этому времени провайдеры преобразованы в фабрики. Мы можем получить их объекты с помощью блока module () .

В случае определения провайдеров реализация метода $ get также обязательна в тестах. Если вам не нужна функциональность, определенная в функции $ get в тестовом файле, вы можете назначить ее пустой функции.

Mocking Modules

Если для модуля, загружаемого в тестовый файл, требуется набор других модулей, тестируемый модуль не может быть загружен, пока не будут загружены все необходимые модули. Загрузка всех этих модулей иногда приводит к плохим тестам, так как некоторые из реальных методов обслуживания могут быть вызваны из тестов. Чтобы избежать этих трудностей, мы можем создать фиктивные модули для загрузки тестируемого модуля.

Например, предположим, что следующий код представляет модуль с добавленным примером сервиса:

 angular.module('first', ['second', 'third'])
  //util and storage are defined in second and third respectively
  .service('sampleSvc', function(utilSvc, storageSvc) {
    //Service implementation
  });

Следующий код является блоком beforeEach в тестовом файле примера сервиса:

 beforeEach(function() {
  angular.module('second',[]);
  angular.module('third',[]);
  
  module('first');
  
  module(function($provide) {
    $provide.service('utilSvc', function() {
      // Mocking utilSvc
    });

    $provide.service('storageSvc', function() {
      // Mocking storageSvc
    });
  });
});

В качестве альтернативы, мы также можем добавить фиктивные реализации сервисов к фиктивным модулям, определенным выше.

Насмешливые методы, возвращающие обещания

Может быть сложно написать сквозное приложение Angular без обещаний. Это становится проблемой для тестирования фрагмента кода, который зависит от метода, возвращающего обещание. Простой шпион Jasmine приведет к провалу некоторых тестовых случаев, так как тестируемая функция ожидает объект со структурой фактического обещания.

Асинхронные методы можно смоделировать с помощью другого асинхронного метода, который возвращает обещание со статическими значениями. Рассмотрим следующую фабрику:

 angular.module('moduleUsingPromise', [])
  .factory('dataSvc', function(dataSourceSvc, $q) {
    function getData() {
      var deferred = $q.defer();

      dataSourceSvc.getAllItems().then(function(data) {
        deferred.resolve(data);
      }, function(error) {
        deferred.reject(error);
      });

      return deferred.promise;
    }

    return {
      getData: getData
    };
  });

Мы протестируем функцию getData () на вышеупомянутой фабрике. Как видим, это зависит от метода getAllItems () сервиса dataSourceSvc . Нам нужно смоделировать сервис и метод перед тестированием функциональности метода getData () .

У службы $ q есть методы when () и reject (), которые позволяют разрешать или отклонять обещание со статическими значениями. Эти методы пригодятся в тестах, которые имитируют метод, возвращающий обещание. Следующий фрагмент проверяет фабрику dataSourceSvc :

 module(function($provide) {
  $provide.factory('dataSourceSvc', function($q) {
    var getAllItems = jasmine.createSpy('getAllItems').andCallFake(function() {
      var items = [];

      if (passPromise) {
        return $q.when(items);
      }
      else {
        return $q.reject('something went wrong');
      }
    });

    return {
      getAllItems: getAllItems
    };
  });
});

Обещание $ q завершает свое действие после следующего цикла дайджеста. Цикл дайджеста продолжается в реальном приложении, но не в тестах. Итак, нам нужно вручную вызвать $ rootScope. $ Digest () для принудительного выполнения обещания. Следующий фрагмент кода показывает пример теста:

 it('should resolve promise', function() {
  passPromise = true;

  var items;

  dataSvcObj.getData().then(function(data) {
    items=data;
  });
  rootScope.$digest();

  expect(mockDataSourceSvc.getAllItems).toHaveBeenCalled();
  expect(items).toEqual([]);
});

Дразнящие глобальные объекты

Глобальные объекты поступают из следующих источников:

  1. Объекты, которые являются частью глобального объекта ‘окна’ (например, localStorage, indexedDb, Math и т. Д.).
  2. Объекты, созданные сторонней библиотекой, такой как jQuery, подчеркивание, момент, ветерок или любая другая библиотека.

По умолчанию глобальные объекты не могут быть смоделированы. Нам нужно выполнить определенные шаги, чтобы сделать их насмешливыми.

Мы можем не захотеть высмеивать служебные объекты, такие как функции объекта Math или _ (созданные библиотекой Underscore), поскольку их операции не выполняют никакой бизнес-логики, не манипулируют пользовательским интерфейсом и не общаются с источник данных. Но такие объекты, как $ .ajax, localStorage, WebSockets, breeze и toastr, должны быть смоделированы. Потому что, если их не смоделировать, эти объекты будут выполнять свою фактическую работу при выполнении модульных тестов, и это может привести к некоторым ненужным обновлениям пользовательского интерфейса, сетевым вызовам, а иногда и ошибкам в тестовом коде.

Каждый фрагмент кода, написанный на Angular, тестируется из-за внедрения зависимостей. DI позволяет нам пропускать любой объект, который следует за оболочкой фактического объекта, чтобы просто заставить тестируемый код не прерываться при его выполнении. Глобальные объекты могут быть смоделированы, если они могут быть введены. Есть два способа сделать глобальный объект инъекционным:

  1. Введите $ window в сервис / контроллер, которому нужен глобальный объект, и получите доступ к глобальному объекту через $ window. Например, следующий сервис использует localStorage через $ window:
 angular.module('someModule').service('storageSvc', function($window) {
  this.storeValue = function(key, value) {
    $window.localStorage.setItem(key, value);
  };
});
  1. Создайте значение или константу, используя глобальный объект, и вставьте его, где это необходимо. Например, следующий код является константой для toastr:
 angular.module('globalObjects',[])
  .constant('toastr', toastr);

Я предпочитаю использовать константу сверх значения для обертывания глобальных объектов, поскольку константы могут быть введены в блоки конфигурации или поставщики, а константы не могут быть декорированы.

Следующий фрагмент демонстрирует насмешку над localStorage и toastr:

 beforeEach(function() {
  module(function($provide) {
    $provide.constant('toastr', {
      warning: jasmine.createSpy('warning'),
      error: jasmine.createSpy('error')
    });
  });

  inject(function($window) {
    window = $window;

    spyOn(window.localStorage, 'getItem');
    spyOn(window.localStorage, 'setItem');
  });
});

Вывод

Насмешка является одной из важных частей написания юнит-тестов на любом языке. Как мы видели, внедрение зависимостей играет важную роль в тестировании и макете. Код должен быть организован таким образом, чтобы функциональность легко тестировалась. В этой статье приведен список наиболее распространенных наборов объектов при тестировании приложений AngularJS. Код, связанный с этой статьей, доступен для скачивания с GitHub .