Search code examples
angularjsunit-testingmocha.jssinonsinon-chai

How do I mock angular's $http with sinon?


I am trying to do a simple mock of angular's $http with sinon in a Mocha test.

But my spy never has any results in it no matter what I try.

searchResource.typeAhead is my function under test. It calls $http based on its arguments and I want to make sure the request is correct.

searchResource.typeAhead returns a promise, but I tried putting the checking code in .then() and it never executes.

suite('Search Resource', function() {

  var injector = angular.injector(['cannonball-client-search', 'cannonball-client-core']);
  var searchResource = injector.get('SearchResource');

  suite('#typeAhead()', function () {
    setup(function () {
      this.server = sinon.fakeServer.create();
      this.server.respondWith('GET',
        config.endpoints.search.typeAhead,
        [200, {'Content-Type': 'application/json'}, '[{ "id": 12, "comment": "Hey there" }]']);
      this.spyhttp = sinon.spy(injector.get('$http'));
    });
    teardown(function () {
      this.server.restore();
    });
    test('positive', function (done) {
      searchResource.typeAhead(
        'expl',
        [{entityType: 'itementity'}],
        [{createdBy: 'Eric'}, {createdBy: 'Tal'}],
        10
      );
      this.server.respond();
      expect(this.spyhttp.calledWith({
        method: 'get',
        url: config.endpoints.search.typeAhead +
        '?query=expl&filter=entityType:itementity&orfilter=createdBy:Eric&orfilter=createdBy:Tal&limit=10'
      })).to.be.true();
      done();
    });
  });
});

Solution

  • The problem lies outside of Sinon mocking.

    If angular.injector is used directly instead of suggested angular.mock.module and angular.mock.inject helpers, the one is on his own with it and his knowledge of Angular injector.

    The obvious downside is that the injector won't be torn down automatically after each spec (while it would be when angular.mock.module is used), so all nested specs operate on the same instance of Angular injector.

    At this point

      var searchResource = injector.get('SearchResource');
    

    SearchResource service instance was already injected with unmocked $http, that's the end of the story. Even if it wouldn't, there's no chance that Angular will ever know that this.spyhttp spy should be used instead of original $http service. Its methods can be spied after the instantiation

    sinon.spy($http, 'get');
    

    but not $http function itself.

    The strategy for testing with angular.injector may be

    var $httpSpy;
    var injector = angular.injector([
      'cannonball-client-search',
      'cannonball-client-core',
      function ($provide) {
        $provide.decorator('$http', function ($delegate) {
          return ($httpSpy = sinon.spy($delegate));
        });
      }
    ]);
    
    // injector.get('$http') === $httpSpy;
    

    Notice that this will make Sinon spy on $http function, not on its methods.

    If the question is about how Angular mocks should be approached with Sinon, then it's as easy as that. Otherwise this may indicate an XY problem, and the other answer addresses it directly ($httpBackend and the way $http embraces it are there exactly to make the burden of mocking XMLHttpRequest requests non-existent).