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