Search code examples
angularjsunit-testingkarma-jasmineangular-resource

Angular and Karma testing a service that uses $resource and returns a promise


I am trying to develop a Unit Test using Jasmine for my AngularJS User service that relies on $resource. My test which is:

'use strict';

describe("User Service Test", function() {

var service;
var mockLoginUser = { email: 'hidden', password: "hidden" };

  beforeEach(module('flightlottery.userApi'));
  beforeEach(inject(function(User) {
    service = User;
  //  $scope = _$scope_;
    //http = $httpBacked;
  }));

it('should fetch login a user', function(done) {
    var testUser = function(user) {
        console.log('callback called');
      //expect(user.email).toBe(mockLoginUser.email);
      //expect(user.password).toBe(mockUser.password);
    };

    var failTest = function(error) {
      expect(error).toBeUndefined();
    };

    //http.expectPost('/users/login', mockLoginUser).respond(200,'');

    //http.expectGET('/employees/1').respond(200,mockEmployee);

    service.login(mockLoginUser)
      .$promise.then(testUser)
      .catch(failTest)
      .finally(done);

//    $scope.$apply;
    //http.flush();
});
});

When I run my test I get the following error.

Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

Can anyone let me know how to accomplish this? I find it rather confusing.

Thanks!

***EDIT: Here is my user service. I would like to test the login method.

angular.module('flightlottery.userApi', ['ngResource']).
factory('User', function($resource, $rootScope) {

  var current_user;

  var User = $resource('http://somesite.ca/api/users/:method/:id', {}, {
    query: {method:'GET', params: {method:'index'}, isArray:true },
    save: {method:'POST', params: {method:'save'} },
    get: {method:'GET', params: {method:'edit'} },
    phistory: {method:'GET', params: {method:'history'}, isArray:true },
    remove: {method:'DELETE', params: {method:'remove'} },
    login: {method:'POST', params: {method:'login'} },
    logout: {method:'POST', params: {method:'logout'} },
    register: {method:'POST', params: {method:'register'} }
  });


  User.setCurrentUser = function(user) {
    //var self = this;
    current_user = user;
    $rootScope.$broadcast('user:updated',user);
    //console.log(self.current_user_id);
  }

  User.getCurrentUser = function() {
    //var self = this;
    return current_user;
    //console.log(self.current_user_id);
  }

  User.registerUser = function(cb) {
    //console.log(cb);
    return User.register(cb);
  }

  User.play_history = function(cb) {
    //console.log(cb);
    return User.phistory(cb);
  }

  User.loginUser = function(cb) {
    return User.login(cb);
  }

  User.logoutUser = function(cb) {
    current_user = null;
    return User.logout(cb);
  //  return User.logout();
    ///return User.save({id: this.id},
        //angular.extend({}, this, {id:undefined}), cb);
  };

  User.prototype.update = function(cb) {
    return User.save({id: this.id},
        angular.extend({}, this, {id:undefined}), cb);
  };

  User.prototype.destroy = function(cb) {
    return User.remove({id: this.id}, cb);
  };

  return User;
});

Solution

  • After trying to guess what you want to accomplish, little refactoring – the results looks like this.

    I'm always wonder what really we want test and what we want to prove if test pass or fail.

    Version with dependencies stubbed

    angular.module('flightlottery.userApi', ['ngResource'])
      .factory('User', function($resource) {
        var User = $resource('http://somesite.ca/api/users/:method/:id', {}, {
          login: {
            method: 'POST',
            params: {
              method: 'login'
            }
          }
        });
    
        User.loginUser = function(cb) {
          return User.login(cb);
        }
    
        return User;
      })
    
    describe("User Service Test", function() {
    
      var service;
      var queryDeferred;
      var mockLoginUser = {
        email: 'hidden',
        password: "hidden"
      };
    
      var scenarios = {
        success: function(user) {
          expect(user.email).toBe(mockLoginUser.email);
          expect(user.password).toBe(mockLoginUser.password);
        },
    
        fail: function(error) {
          expect(error).toBeDefined();
        }
      }
    
      beforeEach(module('flightlottery.userApi'));
    
      beforeEach(inject(function(_$rootScope_) {
        $rootScope = _$rootScope_;
      }));
    
      beforeEach(inject(function($q) {
        queryDeferred = $q.defer();
        mockUserLogin = {
          login: function() {
            return {
              $promise: queryDeferred.promise
            };
          }
        }
    
        spyOn(mockUserLogin, 'login').and.callThrough();
        spyOn(scenarios, 'success').and.callThrough();
        spyOn(scenarios, 'fail').and.callThrough();
      }))
    
    
      it('runs `success scenario` if user object is fetched', function() {
        queryDeferred.resolve(mockLoginUser)
    
        userLogin(mockLoginUser, scenarios);
    
        expect(scenarios.success).toHaveBeenCalled()
        expect(scenarios.fail).not.toHaveBeenCalled()
      });
    
      it('runs `fail scenario` if user object is not fetched', function() {
        var reason = {
          error: 'some error'
        }
        queryDeferred.reject(reason)
    
        userLogin(mockLoginUser, scenarios)
    
        expect(scenarios.success).not.toHaveBeenCalled()
        expect(scenarios.fail).toHaveBeenCalledWith(reason)
      });
    
      function userLogin(mockLoginUser, scenarios) {
        mockUserLogin.login(mockLoginUser)
          .$promise.then(scenarios.success)
          .catch(scenarios.fail)
          .finally();
    
        $rootScope.$apply();
      }
    });
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
    <script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-resource.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>

    Second version - injected service and $httpBackend

    When an Angular application needs some data from a server, it calls the $http service, which sends the request to a real server using $httpBackend service. With dependency injection, it is easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify the requests and respond with some testing data without sending a request to a real server.

    angular.module('flightlottery.userApi', ['ngResource']).
    factory('User', function($resource, $rootScope) {
    
      var User = $resource('http://somesite.ca/api/users/:method/:id', {}, {
        login: {
          method: 'POST',
          params: {
            method: 'login'
          }
        }
      });
    
      User.loginUser = function(cb) {
        return User.login(cb);
      }
    
      return User;
    });
    
    
    describe("User Service Test", function() {
    
      var $httpBackend, User;
    
      beforeEach(module('flightlottery.userApi'));
    
      beforeEach(inject(function(_$httpBackend_, _User_) {
        $httpBackend = _$httpBackend_;
        User = _User_;
      }));
      
      afterEach(function() {
         $httpBackend.verifyNoOutstandingExpectation();
         $httpBackend.verifyNoOutstandingRequest();
       });
    
      it('calls `POST` method to interacts with backend', function() {
        var stubUser = {
          name: 'Some name',
          password: 'somePassword'
        };
        var stubResponse = {
          login: 'someName',
          lastLogin: Date.now()
        }
        
        spyOn(User, 'login').and.callThrough();
        
        User.loginUser(stubUser).$promise.then(function(response) { // response is stubbed by second argument of repond method
          expect(response.login).toBe(stubResponse.login)
          expect(response.lastLogin).toBe(stubResponse.lastLogin)
        });
        
        expect(User.login).toHaveBeenCalledWith(stubUser)
        $httpBackend.expectPOST('http://somesite.ca/api/users/login').respond(200, stubResponse)
        $httpBackend.flush();
      });
    });
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
    <script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-resource.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>