andyhasit
andyhasit

Reputation: 15329

Mock angular factory that uses $q in Jasmine

Surely this has been asked before but I can't find it. I need to mock a factory, but the mock itself needs to use $q, and I end up in a chicken and egg situation with regards to calling module() after inject().

I looked at this question which advises doing a spyOn, which works for services because it is a singleton, but I am calling new on the function returned by my factory, creating a new instance each time, so that won't work...

var app = angular.module('app', []);

app.factory('MyDependencyFactory', function() {
  return function() {
    this.doPromise = function () {
      var defer = $q.defer();
      //obviously more complicated.
      defer.resolve();
      return defer.promise;   
    }
  }
});

app.factory('MyConsumingFactory', function(MyDependencyFactory) {
 return function() {
   var dependency = new MyDependencyFactory();
   this.result;

   this.doSomething = function () {
     dependency.doPromise().then(
       function (data) {
         this.result = data;
       },
       function (error) {
         console.log(error);
       }
       );
   }
  }
});

Jasmine test:

describe('MyConsumingFactory', function() {
  var MyConsumingFactory;

  beforeEach(function () {
    module('app');

    inject( function (_MyConsumingFactory_) {
      MyConsumingFactory = _MyConsumingFactory_;
    });

    inject( function ($q) {
      mockMyDependencyFactory = function () {
        this.doPromise = function (data) {
            var defer = $q.defer();
            defer.resolve('mock data');
          };
        };
    });

    module( function ($provide) {
      $provide.factory('MyDependencyFactory', mockMyDependencyFactory);
    });
  });

  it('works correctly', function () {
    MyConsumingFactory.doSomething();
    $rootScope.$apply();
    expect(MyConsumingFactory.result).toEqual('mock data');
  });

});

I need my mockMyDependencyFactory to use $q, so I need to wrap it in inject( function(..., and I need to do this before the call to module( function ($provide) {... which of course give me:

Error: Injector already created, can not register a module!

Any suggestions on how I get round this?

Or if you think my design flawed (I suppose I could instantiate a MyDependencyFactory and pass that during instantiation of MyConsumingFactory instead of using angular's DI?) I'm all ears :)

Upvotes: 1

Views: 1180

Answers (1)

Michael Radionov
Michael Radionov

Reputation: 13319

First of all, all your calls to module() should be before inject(), otherwise you will get this error: Injector already created, can not register a module! i.e. you should register modules before you inject them in code. Knowing that, we need to mock MyDependencyFactory before injecting, but how do we get $q in there if it is only available in inject()? Actually, it is a common technique in angular tests, to assign injected service to a global variable in a test suite, and then use it across all scenarios:

describe('some suite', function () {

    // "global" variables for injected services
    var $rootScope, $q;

    beforeEach(function () {

        module('app');

        module(function($provide) {

            $provide.factory('MyDependencyFactory', function () {
                return function () {
                    this.doPromise = function (data) {
                        // use "globals"
                        var defer = $q.defer();
                        defer.resolve('mock data');
                        return defer.promise;
                    };
                };   
            });

        });

        inject(function (_$rootScope_, _$q_) {
            // assign to "globals"
            $rootScope = _$rootScope;
            $q = _$q;
        });
    });

    // ....

});

The reason you can use $q in a $provide block is that it is not being used immediately, it will be used only when you call a mocked method or create an instance of a mocked object. By that time, it will be injected and assigned to a global variable $q and have an appropriate value.

One more trick you could do if you want to resolve your promise with different values several times, is to create a global variable defer and initialize it not inside specific method, but in some beforeEach block, and then do defer.resolve('something') inside your scenario with a value you want in this particular scenario.

Here you can see a working example of your code, I've made some extra fixes to make it work (has comments).

Note: I am saying "global" variable, but it is not actually global as in JS terminology, but global within a particular test suite.

Upvotes: 5

Related Questions