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


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

во-первых, давайте посмотрим на какой-то абстрактный контроллер.

function Ctrl($scope, anyService) {

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

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

}

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

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

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

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

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

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

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

4 56

4 ответа:

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

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();
              };
       });

теперь вы можете модульный тест с издевается над Ctrl а также "util".

предоставленная вами функция контроллера будет использоваться 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(...) или использовать угловые $controller сервис для получения Ctrl экземпляр объекта возвращенный будет содержать

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

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

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

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

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

мы прикрепляем частные функции к функции контроллера (тем самым делая их общедоступными, что позволяет издеваться). Чтобы избежать необходимости повторять имя контроллера все время и сделать синтаксис более привлекательным, мы создаем 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();

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