I'm defining some setup code in the config
function of an Angular module
that I want to unit test. It is unclear to me how I should do this. Below is a simplified testcase that shows how I'm getting stuck:
'use strict';
angular.module('myModule', []).config(['$http', '$log', function($http, $log) {
$http.get('/api/getkey').then(function success(response) {
$log.log(response.data);
});
}]);
describe('myModule', function() {
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
angular.module('myModule');
$httpBackend.flush();
}));
});
This is clearly not the right way because I get the following error:
Error: No pending request to flush !
A complete, ready-to-run Angular project with the above testing code can be found on GitHub. If you know what to do with this scenario, please answer here on Stack Overflow. Bonus points if you also submit a pull request to the GitHub repo.
Fixtures have access to a debugElement , which will give you access to the internals of the component fixture. Change detection isn't done automatically, so you'll call detectChanges on a fixture to tell Angular to run change detection.
TestBed is the primary api for writing unit tests for Angular applications and libraries.
Karma is a task runner for our tests. It uses a configuration file in order to set the startup file, the reporters, the testing framework, the browser among other things. The rest of the dependencies are mainly reporters for our tests, tools to use karma and jasmine and browser launchers.
Use run
instead of config
if your initialization requires services to be injected. The config
function can only receive providers and constants as parameters, not instantiated services like $http
(relevant docs).
angular.module('myModule', []).run(['$http', '$log', function($http, $log) {
...
}]);
Initialize your module for testing
beforeEach(module('myModule'));
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
$httpBackend.flush();
}));
So the full working version looks like
'use strict';
angular.module('myModule', []).run(['$http', '$log', function($http, $log) {
$http.get('/api/getkey').then(function success(response) {
$log.log(response.data);
});
}]);
describe('myModule', function() {
beforeEach(module('myModule'));
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
$httpBackend.flush();
}));
});
Also, here's an example of testing the config block to check that a method on a provider was called: https://medium.com/@a_eife/testing-config-and-run-blocks-in-angularjs-1809bd52977e#71e0
mzulch is right to point out that services cannot be injected in an angular.module(...).config
block. He also provides the right solution for the scenario where you actually need to use services in module initialization code: use the .run
block instead of the .config
block. His answer works perfectly for this scenario.
The question of how to write a unit test for the .config
block remains. Let's adapt the naieve code from my question to a scenario where .config
is actually warranted. The following snippet injects a provider dependency instead of a service dependency:
angular.module('myModule', []).config(['$httpProvider', function($httpProvider) {
$httpProvider.useApplyAsync(true);
}]);
describe('myModule', function() {
it('configures the $http service to combine response processing via $applyAsync', inject(function($httpProvider) {
angular.module('myModule');
expect($httpProvider.useApplyAsync()).toBeTruthy();
}));
});
This time, the implementation of 'myModule'
is correct. The unit test however, which is analogous to the attempt in my question, is still incorrect. Now Karma gives me the following error:
Error: [$injector:unpr] Unknown provider: $httpProviderProvider <- $httpProvider
This cryptical error is coming from the inject
which is passed as the second argument to the it
. Note how Provider
is being stuttered. This is caused by the fact that inject
is looking for the provider for $httpProvider
. A "meta provider", as we may call it. Such things don't exist in the Angular framework, but inject
is trying it anyway because it expects you to only ask for service dependencies. Services do have providers, for example, $http
has $httpProvider
.
So inject
(full name: angular.mock.inject
, here available globally) is not the right way to get hold of $httpProvider
in the testcase. The right way is to define an anonymous module configuration function using module
(angular.mock.module
) which closes over a variable in which we can capture the provider. This works because providers can be injected at configuration time (see the link at the bottom of mzulch's answer as well as my own answer to my other question for details on configuration time vs run time). It looks like this:
var $httpProvider;
beforeEach(function() {
module(function(_$httpProvider_) {
// this is a .config function
$httpProvider = _$httpProvider_;
});
// after this I can use inject() to make the magic happen
});
Another mistake in my naieve testcase is that I'm trying to execute 'myModule'
s configuration steps by calling angular.module('myModule')
. For testcase purposes, I should be using the global module
(angular.mock.module
) instead, and the wisest place to do so is in the beforeEach
fixture. In conclusion, the following code does the job:
describe('myModule', function() {
var $httpProvider;
beforeEach(function() {
module(function(_$httpProvider_) {
$httpProvider = _$httpProvider_;
});
module('myModule');
});
it('configures the $http service to combine response processing via $applyAsync', function() {
inject(); // enforces all the module config steps
expect($httpProvider.useApplyAsync()).toBeTruthy();
});
});
I opted to put the inject()
at the start of my testcase, but I could also put it at the end of the beforeEach
. The advantage of the latter approach would be that I can write the call to inject
in one place and not need to repeat it in every testcase. The advantage of the approach actually taken here is that more modules can be added to the injector in later beforeEach
es or even in individual testcases.
I pushed this alternative solution to a new branch on GitHub.
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