jbernal
jbernal

Reputation: 786

AngularJS: How to use $q in your config phase for a unit test?

I have an angular service responsible for loading a config.json file. I would like to call it in my run phase so I set that json in my $rootContext and hence, it is available in the future for everyone.

Basically, this is what I've got:

angular.module('app.core', []).run(function(CoreRun) {
    CoreRun.run();
});

Where my CoreRun service is:

 angular.module('app.core').factory('CoreRun', CoreRun);

 CoreRun.$inject = ['$rootScope', 'config'];

 function CoreRun($rootScope, config) {
   function run() {
     config.load().then(function(response) {
       $rootScope.config = response.data;
     });
   }    
   return {
     run: run
   };
}

This works fine and the problem comes up when I try to test it. I want to spy on my config service so it returns a fake promise. However, I cannot make it since during the config phase for my test, services are not available and I cannot inject $q.

As far as I can see the only chance I have to mock my config service is there, in the config phase, since it is called by run block.

The only way I have found so far is generating the promise using jQuery which I really don't like.

beforeEach(module('app.core'));

var configSample;

beforeEach(module(function ($provide) {
   config = jasmine.createSpyObj('config', [ 'load' ]);
   config.load.and.callFake(function() {
     configSample = { baseUrl: 'someurl' };        
     return jQuery.Deferred().resolve({data: configSample}).promise();
   });
   provide.value('config', config);
}));

it('Should load configuration using the correspond service', function() {
  // assert
  expect(config.load).toHaveBeenCalled();
  expect($rootScope.config).toBe(configSample);
});

Is there a way to make a more correct workaround?

EDIT: Probably worth remarking that this is an issue just when unit testing my run block.

Upvotes: 6

Views: 1485

Answers (1)

Michael Radionov
Michael Radionov

Reputation: 13319

Seems that it is not possible to inject $q the right way, because function in your run() block fires immediately. run() block is considered a config phase in Angular, so inject() in tests only runs after config blocks, therefore even if you inject() $q in test, it will be undefined, because run() executes first.

After some time I was able to get $q in the module(function ($provide) {}) block with one very dirty workaround. The idea is to create an extra angular module and include it in test before your application module. This extra module should also have a run() block, which is gonna publish $q to a global namespace. Injector will first call extra module's run() and then app module's run().

angular.module('global $q', []).run(function ($q) {
    window.$q = $q;
});

describe('test', function () {

    beforeEach(function () {

        module('global $q');

        module('app.core');

        module(function ($provide) {
            console.log(window.$q); // exists
        });

        inject();

    });
});

This extra module can be included as a separate file for the test suite before spec files. If you put the module in the same file where the tests are, then you don't event need to use a global window variable, but just a variable within a file.

Here is a working plunker (see a "script.js" file)

First solution (does not solve the issue):

You actually can use $q in this case, but you have to inject it to a test file. Here, you won't really inject it to a unit under test, but directly to a test file to be able to use it inside the test. So it does not actually depend on the type of a unit under test:

// variable that holds injected $q service
var $q;

beforeEach(module(function ($provide) {
    config = jasmine.createSpyObj('config', [ 'load' ]);

    config.load.and.callFake(function() {
        var configSample = { baseUrl: 'someurl' };

        // create new deferred obj
        var deferred = $q.defer();

        // resolve promise
        deferred.resolve({ data: configSample });

        // return promise
        return deferred.promise;
   });

   provide.value('config', config);
}));

// inject $q service and save it to global (for spec) variable
// to be able to access it from mocks
beforeEach(inject(function (_$q_) {
    $q = _$q_;
}));

Resources:

And one more note: config phase and run phase are two different things. Config block allows to use providers only, but in the run block you can inject pretty much everything (except providers). More info here - Module Loading & Dependencies

Upvotes: 4

Related Questions