即使有范围,Angularjs单元测试监视回调也不会被调用。$ digest

时间:2014-07-09 06:30:08

标签: angularjs unit-testing angularjs-scope watch

我正在努力测试一个监视几个变量的控制器。在我的单元测试中,即使调用scope,我也无法调用$ watch函数的回调。$ 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();
    });
  });
});

我尝试更换所有范围。$ digest()调用范围。$ 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';
    });
    
  2. 测试顺利通过。不确定为什么newValue和oldValue没有正确设置。

    1. 如果我将$ watch回调函数更改为命名函数,并检查是否曾调用过命名函数,那么也会失败。例如,我可以将控制器更改为:

      $scope.update_classrooms = function(newValue, oldValue){
          $scope.test = 'testing';
          console.log('test');
      };
      
      $scope.watch('subject_list', $scope.update_classrooms);
      
    2. 将我的测试改为:

      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'登录到控制台。我感到很困惑。

3 个答案:

答案 0 :(得分:8)

我刚刚在自己的代码库中遇到了这个问题,答案结果是我需要一个范围。$ digest()在实例化控制器后立即调用。在测试中,您必须在观察变量的每次更改后手动调用范围。$ digest()。这包括在构造控制器以记录初始监视值之后。

此外,正如评论中指定的Vitall一样,集合需要$ watchCollection()。

在简单示例中更改此手表可解决此问题:

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

到:

$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行)。

答案 1 :(得分:2)

这是不可测试的原因是因为函数传递给观察者的方式与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,而是传递一个函数,该函数又在“privateMethods”上执行“methodName”函数。

答案 2 :(得分:0)

您的问题是,您需要拨打$watch而不是普通$watch

scope.subject_list = ['new item']只会响应作业(即$watchCollection)。 除了分配,A将响应列表的更改(推/拼接)。