How to test _.defer() using Jasmine, AngularJs

2019-02-20 03:31发布

问题:

I already asked this question where the main point was scope doesn't exists in terminal but it exists in Chrome debugging tool. Despite the answers it didn't get fixed.

The question is what is the right syntax to test the bellow directive, especially the line expect(scope.measurementScroll).toBe(true);. While digging through web i couldn't find any similar question most questions are related to $q.defer() where in my case there is underscore method _.defer()

Controller

'use strict';
angular.module('myApp')
  .controller('MeasurementsTimelineCtrl', ['$scope', '$state', 'Measurements', function($scope, $state, Measurements) {
    $scope.measurements = null;
    var userId = $scope.currentUser ? $scope.currentUser.id : null;
    if (userId) {
      var listOfMeasurements = Measurements.userIndex(userId);
      listOfMeasurements.then(function(data){
        $scope.measurements = data;
        $scope.$broadcast('measurements-updated', $scope.measurements);
      });
    }
  }]);

Directive:

'use strict';
angular.module('myApp')
  .directive('dashboardMeasurementTimeline', ['$window', function($window) {
    return {
      restrict: 'E',
      templateUrl: 'myView.html',
      controller: 'MeasurementsTimelineCtrl',
      link: function(scope, element){
        scope.$on('measurements-updated', function(measurements) {
          _.defer(function(){
            if(measurements) {
              scope.measurementScroll = true;
            }
          });
        });
      }
    };
  }]);

Test

'use strict';
describe('Directive: dashboardMeasurementTimeline', function () {

  var $rootScope, $compile, element, scope;

  beforeEach(function() {
    module('myApp');

    inject(function($injector) {
      $rootScope = $injector.get('$rootScope');
      $compile = $injector.get('$compile');
    });

    scope = $rootScope.$new();
    element = angular.element('<dashboard-measurement-timeline></dashboard-measurement-timeline>');
    element = $compile(element)(scope);

    scope.currentUser = {id : 'someId'};
    scope.$digest();
    scope.measurements = [{id: 'someId', time_of_test: 'Tue, 30 Dec 2014 14:00:00 -0000'},
      {id: 'someId', time_of_test: 'Thu, 20 Nov 2014 03:00:00 -0000'},];
    scope.$broadcast('measurements-updated', scope.measurements);
    scope.$apply();
  });

  it('should assign true value to measurementScroll', function () {
    expect(scope.measurementScroll).toBe(true);
  });
});

回答1:

You can do this by injecting a mock underscore library, with a defer function defined in the test. A way to do this is to define your own factory, _, which can then be mocked easily:

app.factory('_', function($window) {
  return $window._;
});

Then in the directive, you have to use it by injecting it:

app.directive('dashboardMeasurementTimeline', ['_', function(_) {

In the test, you can then mock it:

var deferCallback;
beforeEach(module(function($provide) {
  deferCallback = null;
  $provide.value('_', {
    defer: function(callback) {
      deferCallback = callback;
    }
  });
}));

This means that instead of the real one, the directive will use the mock _, which saves the callback passed to defer as deferCallback so you can invoke it when needed:

scope.$broadcast('measurements-updated', scope.measurements);
deferCallback();

This makes the test synchronous, which is usually a better idea than using done(), as it keeps test as fast as possible.

You can see the above working at http://plnkr.co/edit/r7P25jKzEFgE5j10bZgE?p=preview



回答2:

If you do not have lodash as a service to be injected you can just spy over the defer method, and if you care about the execution of the function passed then you can just set a callFake and call the argument function passed to the real defer:

spyOn(_, 'defer').and.callFake(f => f());

More deeper let's say you have the following call:

function toTest() {
 _.defer(() => service.callAFunction());
}

then in your test you can say:

it('should call service.callAFunction', () => {
   spyOn(service, 'callAFunction');
   spyOn(_, 'defer').and.callFake(f => f());
   toTest();
   expect(_.defer).toHaveBeenCalled();
   expect(service.callAFunction).toHaveBeenCalled();
}


回答3:

@Michal Charezma, gave a great solution for the problem that is actually a solution, but as it turned out it has some other restrictions for the rest of _ functions. For example:

angular.element(scrollContainer).bind('scroll', _.throttle(scope.disableButtons, 500));

raises an error that the throttle is undefined.

Following @Michal's logic, found another solution that lets functions like _.throttle() to work properly. So, instead of importing _ and using:

app.factory('_', function($window) {
  return $window._;
});

One can mock the defer function only, from the spec like:

var deferCallback = $window._.defer.mostRecentCall.args[0];
deferCallback()