Как написать тестируемые контроллеры с частными методами в AngularJs?

Хорошо, поэтому я долго спотыкался о какой-то проблеме, и мне хотелось бы услышать мнение от остальной части сообщества.

Сначала рассмотрим некоторый абстрактный контроллер.

function Ctrl($scope, anyService) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      util();
   };

   function util() {
      anyService.doSmth();
   }

}

Ясно, что мы имеем здесь:

  • регулярный эшафот для контроллера с $scope и некоторая служебная инъекция
  • некоторое поле и функция, прикрепленные к области
  • частный метод util()

Теперь я хотел бы осветить этот класс в модульных тестах (Жасмин). Однако проблема в том, что я хочу проверить, что при щелчке (вызов whenClicked()) какой-либо элемент, который будет вызываться методом util(). Я не знаю, как это сделать, поскольку в тестах Jasmine я всегда получаю ошибки, которые либо макет для util() не был определен или не был вызван.

Примечание. Я не пытаюсь исправить этот конкретный пример, я прошу о тестировании такого кода в целом. Поэтому, пожалуйста, не говорите мне, "что такое точная ошибка". Я спрашиваю, как это сделать, а не как это исправить.

Я пробовал несколько способов обойти это:

  • Очевидно, я не могу использовать $scope в своих модульных тестах, поскольку у меня нет этой функции, прикрепленной к этому объекту (обычно она заканчивается сообщением Expected spy but got undefined или аналогичным)
  • Я попытался привязать эти функции к объекту контроллера через Ctrl.util = util;, а затем проверил mocks как Ctrl.util = jasmine.createSpy(), но в этом случае Ctrl.util не вызывается, поэтому тесты терпят неудачу
  • Я попытался сменить util() на объект this и снова высмеять Ctrl.util, не повезло

Ну, я не могу найти способ обойти это, я бы ожидал некоторой помощи от ниндзя JS, рабочая скрипка была бы идеальной.

Ответ 1

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

function Ctrl($scope, util) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      util();
   };
}

angular.module("foo", [])
       .service("anyService", function(...){...})
       .factory("util", function(anyService) {
              return function() {
                     anyService.doSmth();
              };
       });

Теперь вы можете unit test с помощью mocks Ctrl + strong > , а также ".

Ответ 2

Функция контроллера, которую вы предоставили, будет использоваться Angular как конструктор; в какой-то момент он будет вызываться с помощью new для создания фактического экземпляра контроллера. Если вам действительно нужны функции в объекте контроллера, которые не подвергаются области $scope, но доступны для шпионажа /stubbing/mocking, вы можете прикрепить их к this.

function Ctrl($scope, anyService) {

  $scope.field = "field";
  $scope.whenClicked = function() {
    util();
  };

  this.util = function() {
    anyService.doSmth();
  }
}

Когда вы вызываете var ctrl = new Ctrl(...) или используете службу Angular $controller для извлечения экземпляра Ctrl, возвращаемый объект будет содержать функцию util.

Вы можете увидеть этот подход здесь: http://jsfiddle.net/yianisn/8P9Mv/

Ответ 3

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

Например, что, если вы понимаете, что утилита использовалась в нескольких местах, но теперь, основываясь на другом рефакторинге кода, он только вызывается в этом одном месте. Зачем нужен дополнительный вызов функции? Просто включите anyService.doSmith() внутри вас $scope.whenClicked(). В приведенных выше предложениях, предполагая, что вы тестируете вызов util(), ваши тесты будут прерываться, даже если вы не изменили функциональность программы. Одним из основных значений модульного тестирования является упрощение рефакторинга без нарушения правил, поэтому, если вы не нарушаете вещи, тест не должен терпеть неудачу.

Что вам нужно сделать, так это убедиться, что при вызове $scope.whenClicked вызывается anyService.doSmth(). Вам просто нужно:

spyOn(anyService,'doSmith')
scope.whenClicked();
expect(anyService.doSmith).toHaveBeenCalled();

Ответ 4

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

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

function Ctrl($scope, anyService) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      self.util();
   };

   var self = Ctrl; // For the sake of syntax simplicity only

   self.util = function() {
      anyService.doSmth();
   };

}

а затем в модульных тестах теперь мы можем использовать:

Ctrl.util = jasmine.createSpy("util()");
expect(Ctrl.util).toHaveBeenCalled();

Мне это все еще не нравится, но я думаю, что это самый простой способ сделать это. Я надеюсь, что кто-то найдет лучший подход.