1

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 :)

Community
  • 1
  • 1
andyhasit
  • 14,137
  • 7
  • 49
  • 51

1 Answers1

5

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.

Michael Radionov
  • 12,859
  • 1
  • 55
  • 72
  • This example works, but if app.run() block calls 'MyDependencyFactory' (which in my real world scenario happens) then in tests it gets the mock, where due to timing $q is undefined: http://plnkr.co/edit/eTGrCRwYA8fSm4yMXw2J?p=preview – andyhasit Jun 23 '15 at 23:56
  • I got around this with an ugly hack inside app.run() that checks a global (real global) variable to determine whether it is running in test mode. – andyhasit Jun 24 '15 at 00:24
  • You also can provide `$q` to a mock as an argument to a factory constructor in the test: `$provide.factory('MyDependencyFactory', function ($q) { ... })`, it will be injected like in the real app. – Michael Radionov Jun 24 '15 at 07:24