0

I have a service called myHttp that returns a promise, and a controller that calls myHttp twice with different parameters.

To test the controller, I'm attempting to mock myHttp with Jasmine spyOn like so:

  beforeEach(inject(function($controller, _$rootScope_, _$q_, myHttp) {
    $scope = _$rootScope_.$new();
    $q = _$q_;
    deferred = $q.defer();
    deferred2 = $q.defer();
    spyOn(myHttp, 'call').and.callFake(fake(1)).and.returnValue(deferred.promise);
    spyOn(myHttp, 'call').and.callFake(fake(2)).and.returnValue(deferred2.promise);

where fake is a function that retrieves the parameters to be used in myHttp call.

Problem is that I cannot declare twice the same mocked function. I get the following error from Jasmine:

Error: call has already been spied upon

How can this test be fixed? this is the PLUNK

Javascript:

angular.module("mymodule", [])

.service('myHttp', function($http,$q){
    this.call = function(obj) {
        var defer = $q.defer();
        $http({url:obj.url, data:obj.data})
              .then(function (response) {
                  defer.resolve(response);
              });
        return defer.promise;
    };
})

.controller('ctl', function($scope,myHttp) {
      $scope.read = function (id){
          var data = {};
          data.id = id;
          myHttp.call({url:'/getStudent', data:data})
              .then(function(response) {
                  $scope.id = response.id;
                  $scope.name = response.nm;
                  $scope.classId = response.clsid; 

                  var data2 = {};
                  data2.id = $scope.classId;
                  myHttp.call({url:'/getClass', data:data2})
                      .then(function(response) {
                          $scope.className = response.nm;
                  });
              });
     };
});



describe('Testing a Controller that uses a Promise', function () {
  var $scope;
  var $q;
  var deferred;
  var $timeout;

  beforeEach(module('mymodule'));

  beforeEach(inject(function($controller, _$rootScope_, _$q_, myHttp) {
    $scope = _$rootScope_.$new();
    $q = _$q_;
    deferred = $q.defer();
    deferred2 = $q.defer();
    spyOn(myHttp, 'call').and.callFake(fake(1)).and.returnValue(deferred.promise);
    spyOn(myHttp, 'call').and.callFake(fake(2)).and.returnValue(deferred2.promise);

    $controller('ctl', { 
      $scope: $scope, 
      myHttp: myHttp
    });

    $scope.read(1)

  }));

  function fake (option) {
    if (option==1)
       return {url:'/getStudent', data: {id: 1}};
    else
       return {url:'/getClass', data: {id: 10}};
  }

  it('should resolve two promises', function () {

    var student = {
      id: 1,
      nm: "John",
      clsid: 10
    };

    var clazz = {
      id: 10,
      nm: "Math"
    };

    deferred.resolve(student);
    $scope.$apply();

    deferred2.resolve(clazz);
    $scope.$apply();

    expect($scope.id).toBe(student.id);
    expect($scope.name).toBe(student.nm);
    expect($scope.classId).toBe(student.clsid);
    expect($scope.className).toBe(clazz.nm);
  });

});
ps0604
  • 1,227
  • 23
  • 133
  • 330

2 Answers2

1

Just spy on myHttp.call once and change your fake function call to take in the request parameters and return an object based on the request.

var student = {
  id: 1,
  nm: "John",
  clsid: 10
};

var clazz = {
  id: 10,
  nm: "Math"
};

spyOn(myHttp, 'call').and.callFake(function(obj) {
  if (obj.url == '/getStudent') {
    return $q.when(student);
  } else if (obj.url = '/getClass') {
    return $q.when(clazz);
  }
  return $q.reject('Mock not supported');
});

See plunker for full working example.

And as a side, your http call be reduced to just

.service('myHttp', function($http,$q){
    this.call = function(obj) {
        return $http({url:obj.url, data:obj.data});
    };
})

because $http already returns a promise.

Also, you can also use the angular $httpBackend to mock your http calls instead of using the jasmine spyon. See this plunker which uses $httpBackend with a slight change to myHttp.call.

logee
  • 5,017
  • 1
  • 26
  • 34
0

The very general answer is that you don't have to create your spy at a top-level beforeEach. You can use a beforeEach that's closer to the individual spec or even create the spy in the it statement. You could also check something in your fake to see which promise you want to return based on the parameter the fake is being called with.

However, what condition are you actually wanting to check for? Usually there's something that should happen after the promise is resolved, and you actually want to look at that. It's easier to give you specific feedback if you say what it is you're aiming for in the end rather than saying what you think is the right solution and asking us how to get to that.

Please see also $q.defer: You're doing it wrong

Amy Blankenship
  • 6,485
  • 2
  • 22
  • 45