Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 987
jbernal Avatar asked Aug 11 '15 10:08

jbernal


People also ask

How to do unit testing in angular js?

Testing in AngularJS is achieved by using the karma framework, a framework which has been developed by Google itself. The karma framework is installed using the node package manager. The key modules which are required to be installed for basic testing are karma, karma-chrome-launcher ,karma-jasmine, and karma-cli.

Which component is used to inject and mock AngularJS service within the unit test?

AngularJS also provides the ngMock module, which provides mocking for your tests. This is used to inject and mock AngularJS services within unit tests.

How to test Directive AngularJS?

If a directive creates its own scope and you want to test against it, you can get access to that directive's scope by doing var directiveScope = myElement. children(). scope() - It will get the element's child (the directive itself), and get the scope for that.

Is AngularJS code unit testable?

Note that unit tests do not replace good coding. AngularJS' documentation is pretty clear on this point: “Angular is written with testability in mind, but it still requires that you do the right thing.” Getting started with writing unit tests — and coding in test-driven development — is hard.


1 Answers

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:

  • Read more about inject() - angular.mock.inject
  • $q

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

like image 56
Michael Radionov Avatar answered Oct 19 '22 00:10

Michael Radionov