Обратный вызов часов модульного теста Angularjs не вызывается даже с областью действия. $ digest

Я борюсь с модульным тестированием контроллера, который следит за парой переменных. В моих модульных тестах я не могу получить обратный вызов для вызова функции $watch, даже при вызове scope.$digest(). Кажется, это должно быть довольно просто, но мне не повезло.

Вот что у меня есть в моем контроллере:

angular.module('app')
  .controller('ClassroomsCtrl', function ($scope, Classrooms) {

    $scope.subject_list = [];

    $scope.$watch('subject_list', function(newValue, oldValue){
      if(newValue !== oldValue) {
        $scope.classrooms = Classrooms.search(ctrl.functions.search_params());
      }
    });
});

И вот мой модульный тест:

angular.module('MockFactories',[]).
  factory('Classrooms', function(){
    return jasmine.createSpyObj('ClassroomsStub', [
      'get','save','query','remove','delete','search', 'subjects', 'add_subject', 'remove_subject', 'set_subjects'
    ]);
  });

describe('Controller: ClassroomsCtrl', function () {

  var scope, Classrooms, controllerFactory, ctrl;

  function createController() {
    return controllerFactory('ClassroomsCtrl', {
      $scope: scope,
      Classrooms: Classrooms
    });
  }

  // load the controller's module
  beforeEach(module('app'));

  beforeEach(module('MockFactories'));

  beforeEach(inject(function($controller, $rootScope, _Classrooms_ ){
    scope = $rootScope.$new();

    Classrooms = _Classrooms_;

    controllerFactory = $controller;

    ctrl = createController();

  }));

  describe('Scope: classrooms', function(){

    beforeEach(function(){
      Classrooms.search.reset();
    });

    it('should call Classrooms.search when scope.subject_list changes', function(){
      scope.$digest();
      scope.subject_list.push(1);
      scope.$digest();

      expect(Classrooms.search).toHaveBeenCalled();
    });
  });
});

Я попытался заменить все вызовы scope.$digest() на вызовы scope.$apply(). Я пытался позвонить им 3 или 4 раза, но я не могу получить обратный вызов $watch для вызова.

Любые мысли о том, что может происходить здесь?

ОБНОВЛЕНИЕ: Вот еще более простой пример, в котором не используются макеты, заглушки или внедрения фабрик.

angular.module('edumatcherApp')
  .controller('ClassroomsCtrl', function ($scope) {
    $scope.subject_list = [];
    $scope.$watch('subject_list', function(newValue, oldValue){
      if(newValue !== oldValue) {
        console.log('testing');
        $scope.test = 'test';
      }
    });

И модульный тест:

it('should set scope.test', function(){
  scope.$digest();
  scope.subject_list.push(1);
  scope.$digest();

  expect(scope.test).toBeDefined();
});

Это также не работает с «Ожидается, что неопределенное будет определено». и ничего не записывается в консоль.

ОБНОВЛЕНИЕ 2

Еще 2 интересные вещи я отметил.

  1. Похоже, одна из проблем заключается в том, что newValue и oldValue совпадают при вызове обратного вызова. Я зарегистрировал оба в консоли, и они оба равны []. Итак, например, если я изменю свою функцию $watch, чтобы она выглядела так:

    $scope.$watch('subject_list', function(newValue, oldValue){
        console.log('testing');
        $scope.test = 'test';
    });
    

тест проходит нормально. Не уверен, почему новые значения и старые значения не устанавливаются правильно.

  1. Если я изменю свою функцию обратного вызова $watch на именованную функцию и просто проверю, вызывалась ли когда-либо именованная функция, это также не удастся. Например, я могу изменить свой контроллер на это:

    $scope.update_classrooms = function(newValue, oldValue){
        $scope.test = 'testing';
        console.log('test');
    };
    
    $scope.watch('subject_list', $scope.update_classrooms);
    

И измените мой тест на это:

it('should call update_classrooms', function(){
  spyOn(scope,'update_classrooms').andCallThrough();
  scope.$digest();
  scope.subject_list.push(1);
  scope.$digest();
  expect(scope.update_classrooms).toHaveBeenCalled();
});

он завершается с ошибкой «Ожидается, что был вызван шпионский update_classrooms».

В этом случае update_classrooms определенно вызывается, потому что в консоли регистрируется «test». Я сбит с толку.


person CTC    schedule 09.07.2014    source источник
comment
Я вижу expect(Classrooms.search).toHaveBeenCalled(), но не вижу, где настроен этот шпион. Что за сообщение об ошибке?   -  person ivarni    schedule 09.07.2014
comment
Извините, я должен был включить это. Я обновил вопрос с помощью макета factory, который я использую для классов. Там нет сообщения об ошибке. Тест просто не проходит. Однако проблема на самом деле не связана с Classrooms.search. Обратный вызов для функции $watch просто не вызывается. Например, если я поместил console.log в функцию обратного вызова, он никогда не сработает, и ничего не будет записано в консоль.   -  person CTC    schedule 09.07.2014
comment
@ivami, выше я добавил еще более простой пример.   -  person CTC    schedule 09.07.2014
comment
Хм, это странно. Я не вижу ничего плохого в том, что ты делаешь, мне все кажется правильным. Должно быть, это одна из тех простых глупых вещей, которые всегда удивительно трудно заметить. Извините, я понятия не имею, в чем причина того, что вы видите.   -  person ivarni    schedule 10.07.2014
comment
Эй, я знаю, что поздно, но... две вещи в вашем последнем обновлении: 1. изменение в контроллере $scope.$watch('subject_list'... на $scope.$watchCollection('subject_list'... 2 ., а в тестовом методе измените expect(scope.update_classrooms).toHaveBeenCalled(); на expect(scope.test).toBe('testing');   -  person Vitall    schedule 23.09.2014


Ответы (3)


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

Кроме того, как указал Виталь в комментариях, вам нужна $watchCollection() для коллекций.

Изменение этих часов в вашем простом примере решает проблему:

$scope.$watch('subject_list', function(newValue, oldValue){
    if(newValue !== oldValue) {
        console.log('testing');
        $scope.test = 'test';
    }
});

to:

$scope.$watchCollection('subject_list', function(newValue, oldValue){
    if(newValue !== oldValue) {
        console.log('testing');
        $scope.test = 'test';
    }
});

Я сделал jsfiddle, чтобы продемонстрировать разницу:

http://jsfiddle.net/ydv8k4zy/ — оригинал с неудачным тестом — ошибка из-за использования $watch, а не $watchCollection

http://jsfiddle.net/ydv8k4zy/1/ — функциональная версия

http://jsfiddle.net/ydv8k4zy/2/ — $watchCollection завершается с ошибкой без начальной области видимости. $digest.

Если вы поэкспериментируете с console.log для второго ошибочного элемента, вы увидите, что часы вызываются с одним и тем же значением для старого и нового после области видимости.$digest() (строка 25).

person brocksamson    schedule 08.10.2014

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

https://jsfiddle.net/jonhartmann/9bacozmg/

var sounds = {};

sounds.beep = function () {
  alert('beep');
};

document.getElementById('x1').addEventListener('click', sounds.beep);

document.getElementById('x2').addEventListener('click', function () {
    sounds.beep = function () {
    alert('boop');
  };
});

document.getElementById('x3').addEventListener('click', function () {
    sounds.beep();
});

Разница между обработчиком x1 и обработчиком x3 заключается в том, что в первом связывании метод передавался напрямую, а во втором метод beep/boop просто вызывает любой метод объекта.

Это то же самое, с чем я столкнулся со своим $watch - я помещал свои методы в объект "privateMethods", передал его и сделал spyOn(privateMethods, 'methodname'), но поскольку мой наблюдатель был в формате $scope.$watch('value', privateMethods.methodName) было уже слишком поздно - мой шпионский код не сработает. Переход на что-то вроде этого пропустил проблему:

$scope.$watch('value', function () {
  privateMethods.methodName.apply(null, Array.prototype.slice.call(arguments));
});

а потом самое ожидаемое

spyOn(privateMethods, 'methodName')
expect(privateMethods.methodName).toHaveBeenCalled();

Это работает, потому что вы больше не передаете privateMethods.methodName по ссылке в $watch, вы передаете функцию, которая, в свою очередь, выполняет функцию «methodName» в «privateMethods».

person Jon Hartmann    schedule 16.02.2016

Ваша проблема в том, что вам нужно вызывать $scope.$watchCollection вместо обычного $watch.

$watch будет отвечать только на задания (например, scope.subject_list = ['new item']). Помимо назначений, $watchCollection будет реагировать на изменения в списках (push/splice).

person clearfix    schedule 27.01.2016