akronymn
akronymn

Reputation: 2446

How to mock an function of an Angular service in Jasmine

I have the following angular controller

function IndexCtrl($scope, $http, $cookies) {   

    //get list of resources
    $http.get(wtm.apiServer + '/v1/developers/me?access_token=' + $cookies['wtmdevsid']).
    success(function(data, status, headers, config) {
        // snip 
      }).
      error(function(data, status, headers, config) {
        // snip
      });

$scope.modal = function() {
      // snip
}

return;
}

What I am trying to do is mock the get method on the $http service. Here's my unit test code:

describe('A first test suite', function(){
    it("A trivial test", function() {
         expect(true).toBe(true);
    });
});

describe('Apps', function(){
describe('IndexCtrl', function(){
    var scope, ctrl, $httpBackend;
    var scope, http, cookies = {wtmdevsid:0};

    beforeEach(inject(function($injector, $rootScope, $controller, $http) {
        scope = $rootScope.$new();

         ctrl = new $controller('IndexCtrl', {$scope: scope, $http: $http, $cookies: cookies});
         spyOn($http, 'get');
         spyOn(scope, 'modal');

    }));

    it('should create IndexCtrl', function() {
        var quux = scope.modal();
        expect(scope.modal).toHaveBeenCalled();
        expect($http.get).toHaveBeenCalled();
    });
  });
});

When I run this I get ReferenceError: wtm is not defined.

wtm is a global object and of course it wouldn't be defined when I run my test because the code that it is declared in is not run when I run my test. What I want to know is why the real $http.get function is being called and how do I set up a spy or a stub so that I don't actually call the real function?

(inb4 hating on globals: one of my coworkers has been tasked with factoring those out of our code :) )

Upvotes: 3

Views: 9653

Answers (3)

Thomas Fankhauser
Thomas Fankhauser

Reputation: 5059

Maybe you could create a custom wrapper mock around $httpBackend that handles your special needs.

In detail, Angular overwrites components of the same name with a last-come first-served strategy, this means that the order you load your modules is important in your tests.

When you define another service with the same name and load it after the first one, the last one will be injected instead of the first one. E.g.:

apptasticMock.service("socket", function($rootScope){
  this.events = {};

  // Receive Events
  this.on = function(eventName, callback){
    if(!this.events[eventName]) this.events[eventName] = [];
    this.events[eventName].push(callback);
  }

  // Send Events
  this.emit = function(eventName, data, emitCallback){
    if(this.events[eventName]){
      angular.forEach(this.events[eventName], function(callback){
        $rootScope.$apply(function() {
          callback(data);
        });
      });
    };
    if(emitCallback) emitCallback();
  }

});

This service offers the exact same interface and behaves exactly like the original one except it never communicates via any socket. This is the service we want to use for testing.

With the load sequence of angular in mind, the tests then look like this:

describe("Socket Service", function(){
  var socket;

  beforeEach(function(){
    module('apptastic');
    module('apptasticMock');

    inject(function($injector) {
      socket = $injector.get('socket');
    });
  });

  it("emits and receives messages", function(){
    var testReceived = false;

    socket.on("test", function(data){
      testReceived = true;
    });

    socket.emit("test", { info: "test" });
    expect(testReceived).toBe(true);
  });

});

The important thing here is that module('apptasticMock') gets executed after module('apptastic'). This overwrites the original socket implementation with the mocked one. The rest is just the normal dependency injection procedure.

This article I wrote could be interesting for you, as it goes into further details.

Upvotes: 1

Kenneth Lynne
Kenneth Lynne

Reputation: 15579

I suggest all globals used the way you described here should be used through the $window service.

All global variables that are available, such as as window.wtm, will also be available on $window.atm.

Then you can stub out your wtm reference completely and spy on it the same way you already described:

 var element, $window, $rootScope, $compile;

  beforeEach(function() {
    module('fooApp', function($provide) {
        $provide.decorator('$window', function($delegate) {

            $delegate.wtm = jasmine.createSpy();

            return $delegate;
        });
    });

    inject(function(_$rootScope_, _$compile_, _$window_) {
        $window = _$window_;
        $rootScope = _$rootScope_;
        $compile = _$compile_;
    });     

  });

Upvotes: 4

Ben Lesh
Ben Lesh

Reputation: 108501

You need to wire up the whenGET method of your $httpBackend in advance of your test. Try setting it up in the beforeEach() function of your test... There is a good example here under "Unit Testing with Mock Backend".

Upvotes: 4

Related Questions