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.
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.
AngularJS also provides the ngMock module, which provides mocking for your tests. This is used to inject and mock AngularJS services within unit tests.
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.
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.
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:
inject()
- angular.mock.inject
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With