How to test the config function of an Angular module?
Asked Answered
I

2

9

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.

Inpatient answered 11/8, 2016 at 15:13 Comment(6)
As @mzulch's answer points out, you can only inject providers (not instances) into config blocks. So how would you expect a unit test to succeed if your code isn't functional?Circumscissile
A question can be misguided (like this question, as I now understand). This can be pointed out in an answer. A great answer explains three things: (1) how to use services globally in a module, (2) how to test config blocks, (3) why you can't have both at the same time.Inpatient
I guess I was more curious about your process. I usually write tests after I'm confident that my code works, but now that I think about it, some people insist on writing tests first and writing their code to fit the test. Fair enough.Circumscissile
There is a third way in between: write the code first, then write the test to check whether the code works. I think that's what I was doing here. Thanks for drawing my attention to this, I had not reflected so precisely on my order of programming and testing before. :-)Inpatient
The fact that you're testing unworkable code doesn't add clarity to the question. Regarding config vs run and serviceName vs serviceNameProvider, see this answer. There are two different injectors for config and run phases which inject service providers and service instances respectively. Service providers can be made to be available in instance injector but not vice versa. That's why $http in config block is a conundrum.Lionhearted
You are right about it complicating the question. For this reason I basically wrote a new version of the question that doesn't have this problem, in my own answer. I would edit the question in place if mzulch had not already answered it so thoroughly. As it stands, I prefer to leave things as they are and award the bounty to mzulch.Inpatient
I
3

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 beforeEaches or even in individual testcases.

I pushed this alternative solution to a new branch on GitHub.

Inpatient answered 19/8, 2016 at 1:7 Comment(0)
P
5

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

Pavkovic answered 15/8, 2016 at 19:37 Comment(3)
I like your answer, but you can further increase its value if you take the insights from that last external link and include them in your answer. After all, the question is how to test the config function. You may alter the example for this purpose, e.g. by replacing $http and $log by providers that can be used in a config block. In addition, it would be nice if you could further explain why $http cannot be injected in the config; after all, the docs also state that services are a special case of providers.Inpatient
I pushed your solution to GitHub.Inpatient
Congratulations with the bounty, I think you deserved it. I will probably accept my own answer, as it more literally addresses the title of the question, but I'm postponing the decision for a bit in case somebody provides arguments I didn't think of.Inpatient
I
3

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 beforeEaches or even in individual testcases.

I pushed this alternative solution to a new branch on GitHub.

Inpatient answered 19/8, 2016 at 1:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.