2

I am trying to test a real http call with Jasmine (integration test), but when i call a method that uses $http.get, it times out and the server never gets called.

I know that I am supposed to inject the implementation of $http but not sure where that should happen.

searchSvc

app.service('searchSvc', ['$http', '$q', searchSvc]);
function searchSvc($http, $q) {
  return {
    search: function(text) {
      console.log('svc.search called with ', text); // this does get called 
      return $q.when($http.get('/search/' + text));
    }
  };
}

searchSpec

describe("searchTest", function() {
  var ctrl, svc, $http;

  beforeEach(function () {
      module('testApp');
    inject(function(_$controller_, searchSvc, _$http_){
      ctrl = _$controller_('searchCtrl');
      svc = searchSvc;
      $http = _$http_;
    })
  });

  it('test server search', function(done) {
      svc.search('re').then(function(result) {
        console.log('promise then'); // this never gets called, because server never gets called
        expect(result).not.toBeNull();
        expect(result.data).not.toBeNull();
        expect(result.data.length).toBeGreaterThan(0);

        done();
      });
  });
Sonic Soul
  • 23,855
  • 37
  • 130
  • 196
  • 1
    I'm not sure it will even allow you to do this... This isn't really a unit test. It's an integration test. I believe the idea is to test your code that calls the server using `$httpBackend` mock, and test the server itself with its own unit tests, and finally write end-to-end tests that would ensure they both function. Those would be run with protractor and test the actual application without mocks. – m59 Sep 13 '14 at 18:58
  • right, i never said it was a unit test. I am trying to use Jasmine for integration tests as well as unit tests – Sonic Soul Sep 13 '14 at 19:15
  • Hmm. I see. Now I'm curious about it as well. I removed angular-mocks, made a reference to $http from an actual angular module and tried that, and it also never resolves the promise. I think there's something that's supposed to happen internally in a normal running application that wouldn't happen here. Kind of like how we have to call `$scope.$digest` manually in tests. – m59 Sep 13 '14 at 19:35
  • I am guessing it's something with the way dependency injection works? I've seen examples with a controller where you can provide implementations of dependencies inside Jasmine block, but couldn't find the notation for services – Sonic Soul Sep 13 '14 at 19:48
  • Check: http://stackoverflow.com/questions/20727205/testing-backend-api-via-http-in-angularjs-karma-jasmine-tests – felipekm Sep 13 '14 at 19:52
  • thanks, not sure how this would work though.. I am trying to test my services, which happen to call the server using $http. So ideally i'd like to call the methods implemented by the service instead of providing my own implementation (in order to achieve the real integration tests) – Sonic Soul Sep 13 '14 at 20:01
  • @SonicSoul ah hah! I figured something out. It does need `$apply`. Now the problem may be that angular-mocks is only going to give you an `$http` that can't be used this way. Check this out: http://jsbin.com/qidiw/33/edit – m59 Sep 13 '14 at 20:02
  • Yes, that seems to be the case. If you want to use anything real like that, you can't use angular-mocks to do it (no `module` or `inject`). Instead, you can write real modules and inject your services into them and test them that way, but this could become a mess of `$apply` issues. I think you're best off just writing unit tests for each thing and then testing E2E with protractor, where your tests are written like human interaction. It seems like you're writing a bad hybrid of a unit test and integration test. I could be wrong.. I'm new to testing myself. – m59 Sep 13 '14 at 20:20
  • @m59 nice! I will test it shortly! – Sonic Soul Sep 13 '14 at 20:39
  • hmmm.. no way to get an instance of the service outside of mocks? I was thinking about doing something like this: _svc = new searchSvc(_$http__); but no luck so far – Sonic Soul Sep 13 '14 at 21:46

3 Answers3

0

In case if you use promises you can find out how to deal with them here http://entwicklertagebuch.com/blog/2013/10/how-to-handle-angularjs-promises-in-jasmine-unit-tests/

Artemis
  • 4,821
  • 3
  • 21
  • 24
  • hmm I'm not sure if this is a promise issue, because the searchSvc.search is triggered. The problem is that the server never gets called – Sonic Soul Sep 14 '14 at 18:52
0

This is sort of hypothetical, but if you include both ngMock & ngMockE2E modules as your app module's dependency (ngMock needs to come before ngMockE2E in the dependency list) you should be able to use $httpBackend service provided by ngMockE2E module to passThrough the search api call to actual backend in your test specs.

Try something like this and see whether it works:

describe("searchTest", function() {
  var ctrl, svc, $httpBackend;

  beforeEach(function () {
    module('testApp');
    inject(function(_$controller_, searchSvc, _$httpBackend_){
      $httpBackend = _$httpBackend_;
      ctrl = _$controller_('searchCtrl');
      svc = searchSvc;
    });
  });

  it('test server search', function(done) {
      $httpBackend.whenGET(/^\/search\//).passThrough();
      svc.search('re').then(function(result) {
        console.log('promise then'); // this never gets called, because server never gets called
        expect(result).not.toBeNull();
        expect(result.data).not.toBeNull();
        expect(result.data.length).toBeGreaterThan(0);

        done();
      });
  });
});
0

Here is a solution that I use to make real HTTP calls when I'm using ngMock for unit tests. I mainly use it for debugging, working through the test, getting JSON examples etc.

I wrote a more detailed post about the solution on my blog: How to Unit Test with real HTTP calls using ngMockE2E & passThrough.

The solution is as follows:

angular.mock.http = {};

angular.mock.http.init = function() {

  angular.module('ngMock', ['ng', 'ngMockE2E']).provider({
    $exceptionHandler: angular.mock.$ExceptionHandlerProvider,
    $log: angular.mock.$LogProvider,
    $interval: angular.mock.$IntervalProvider,
    $rootElement: angular.mock.$RootElementProvider
  }).config(['$provide', function($provide) {
    $provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
    $provide.decorator('$$rAF', angular.mock.$RAFDecorator);
    $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
    $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
    $provide.decorator('$controller', angular.mock.$ControllerDecorator);
  }]);

};

angular.mock.http.reset = function() {

  angular.module('ngMock', ['ng']).provider({
    $browser: angular.mock.$BrowserProvider,
    $exceptionHandler: angular.mock.$ExceptionHandlerProvider,
    $log: angular.mock.$LogProvider,
    $interval: angular.mock.$IntervalProvider,
    $httpBackend: angular.mock.$HttpBackendProvider,
    $rootElement: angular.mock.$RootElementProvider
  }).config(['$provide', function($provide) {
    $provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
    $provide.decorator('$$rAF', angular.mock.$RAFDecorator);
    $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator);
    $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator);
    $provide.decorator('$controller', angular.mock.$ControllerDecorator);
  }]);

};

Include this source file after ngMock, for example:

<script type="text/javascript" src="angular.js"></script>
<script type="text/javascript" src="angular-mocks.js"></script>
<!-- this would be the source code just provided -->
<script type="text/javascript" src="ngMockHttp.js"></script>

How to write the test?

  describe('http tests', function () {

    beforeEach(module('moviesApp'));

    var $controller;
    var $httpBackend;
    var $scope;

    describe('real http tests', function() {

      beforeEach(angular.mock.http.init);
      afterEach(angular.mock.http.reset);

      beforeEach(inject(function(_$controller_, _$httpBackend_) {
        $controller = _$controller_;
        $scope = {};
        $httpBackend = _$httpBackend_;

        // Note that this HTTP backend is ngMockE2E's, and will make a real HTTP request
        $httpBackend.whenGET('http://www.omdbapi.com/?s=terminator').passThrough();
      }));

      it('should load default movies (with real http request)', function (done) {
        var moviesController = $controller('MovieController', { $scope: $scope });

        setTimeout(function() {
          expect($scope.movies).not.toEqual([]);
          done();
        }, 1000);

      });

    });

  });

How it works?

It uses ngMockE2E's version of $httpBackEndProvider, which provides us with the passThrough function we see being used in the test. This does as the name suggests and lets a native HTTP call pass through.

We need to re-define the ngMock module without its fake version of the $BrowserProvider, since that is what prevents the real HTTP calls in unit tests that use ngMock.

Bradley Braithwaite
  • 1,122
  • 2
  • 19
  • 22
  • hmm thanks for posting this! I still find it shocking that it's so hard to do a end to end test in jasmine.. I will certainly try your approach and report back. thanks again – Sonic Soul Jun 16 '15 at 00:29