How to test an AngularJS factory method that returns a $timeout promise with a $http.get inside?

时间:2016-10-20 13:13:37

标签: javascript angularjs unit-testing jasmine

For specific purposes, I had to write a factory method that returns a $timeout promise that, inside of it, returns a $http.get promise.

I want to test if a call to my factory method will call the $http.get with the correct mocked URL (mocked-get-path).

Here is my factory code:

(function() {
  'use strict';

  angular.module('MyApp', []);

  angular.module('MyApp')
    .constant("ROUTES", {
      get: "real-get-path"
    });

  angular.module('MyApp')
    .factory('MyFactory', ['$http', '$timeout', 'ROUTES', MyFactory]);

  function MyFactory($http, $timeout, ROUTES) {
    return {
      myGet: function(id) {
        var random = Math.random() * 1000;
        return $timeout(function () {
            return $http.get(ROUTES.get, {
              params: { id: id }
            })
              .then(function() {
                return response.data;
              });
        }, random);
      }
    };
  }
})();

And my test specification:

describe('simple factory test', function() {
  var $http, $timeout, scope, MyFactory;

  var ROUTES = {
      get: 'mocked-get-path'
  };

  beforeEach(module('MyApp', function ($provide) {
      $provide.constant('ROUTES', ROUTES);
  }));

  beforeEach(module('MyApp'));

  beforeEach(inject(function(_$rootScope_, _$http_, _$timeout_, _MyFactory_) {
    scope = _$rootScope_.$new();
    $http = _$http_;
    $timeout = _$timeout_;
    MyFactory = _MyFactory_;
  }));

  it('should use ROUTES.get on method myGet', function(done) {
    spyOn(Math, "random").and.returnValue(0.01);

    MyFactory.myGet('elem1')
      .then(function(res) {
        expect($http.get).toHaveBeenCalledWith(ROUTES.get);
        done();
      });
    $timeout.flush();
  });
});

You can see that I tried to write a expect for the $http.get inside the then, but it didn't work.

I receive this error from Jasmine:

Error: Unexpected request: GET mocked-get-path?id=elem1

I made a Plunker: https://plnkr.co/edit/Ia6Q6GvKZOkNU2B8GrO1

What am I doing wrong?

2 个答案:

答案 0 :(得分:1)

When testing $http you are going to want to use $httpBackend.when

In your case:

it('should use ROUTES.get on method myGet', function(done) {
  spyOn(Math, "random").and.returnValue(0.01);
  spyOn($http, "get").and.callThrough();

  MyFactory.myGet('elem1')
    .then(function(res) {
      expect($http.get).toHaveBeenCalledWith(ROUTES.get, {params: {id: 'elem1'}});
      done();
    });

  $httpBackend
    .when('GET', "mocked-get-path?id=elem1")
    .respond(200, { foo: 'bar' });

  $timeout.flush(100);

  $timeout.verifyNoPendingTasks();
  $httpBackend.flush();

});

This will cause $httpBackend to complete your request successfully allowing your .then with the expect to execute.

Plunkr showing solution

ngmock fundamentals

I hope this helps! Have a nice day!

答案 1 :(得分:1)

One approach is to add expected calls to the httpBackend like this:

var  $httpBackend;
beforeEach(function () {
        inject(function ( _$httpBackend_ ) {
            $httpBackend = _$httpBackend_;
        }

....

it('....', function(){
    //before the rest of the test
    $httpBackend.expectGET('url').respond("OK");

});

https://docs.angularjs.org/api/ngMock/service/$httpBackend

So, your code will look like this:

describe('simple directive', function() {
  var $http, $timeout, scope, MyFactory, $httpBackend;

  var ROUTES = {
      get: 'mocked-get-path'
  };

  beforeEach(module('MyApp', function ($provide) {
      $provide.constant('ROUTES', ROUTES);
  }));

  beforeEach(module('MyApp'));

  beforeEach(inject(function(_$rootScope_, _$http_, _$timeout_, _MyFactory_, _$httpBackend_) {
    $httpBackend = _$httpBackend_;
    scope = _$rootScope_.$new();
    $http = _$http_;
    $timeout = _$timeout_;
    MyFactory = _MyFactory_;
  }));

  it('should use ROUTES.get on method myGet', function(done) {
    spyOn(Math, "random").and.returnValue(0.01);
    $httpBackend.expectGET("mocked-get-path?id=elem1").respond("OK");
    MyFactory.myGet('elem1')
      .then(function(res) {
        expect($http.get).toHaveBeenCalledWith(ROUTES.get);
        done();
      });
    $timeout.flush();
  });
});

It will get you passed the unexpected GET error. But then there is another issue about not getting the call within the timeout period.

And, providing that now you are expecting a correct call at the $httpBackend, you can simplify your test case by removing the async nature of it like this:

(remove the paremeter done, and simplify the test code)

it('should use ROUTES.get on method myGet', function() {
    spyOn(Math, "random").and.returnValue(0.01);
    $httpBackend.expectGET("mocked-get-path?id=elem1").respond("OK");
    MyFactory.myGet('elem1');
    $timeout.flush();
  });