How to test AngularJS custom provider

21,128

Solution 1

I had this same question and only found a working solution in this Google Group answer and it's referenced fiddle example.

Testing your provider code would look something like this (following the code in the fiddle example and what worked for me):

describe('Test app.config provider', function () {

    var theConfigProvider;

    beforeEach(function () {
        // Initialize the service provider 
        // by injecting it to a fake module's config block
        var fakeModule = angular.module('test.app.config', function () {});
        fakeModule.config( function (configProvider) {
            theConfigProvider = configProvider;
        });
        // Initialize test.app injector
        module('app.config', 'test.app.config');

        // Kickstart the injectors previously registered 
        // with calls to angular.mock.module
        inject(function () {});
    });

    describe('with custom configuration', function () {
        it('tests the providers internal function', function () {
            // check sanity
            expect(theConfigProvider).not.toBeUndefined();
            // configure the provider
            theConfigProvider.mode('local');
            // test an instance of the provider for 
            // the custom configuration changes
            expect(theConfigProvider.$get().mode).toBe('local');
        });
    });

});

Solution 2

i've been using @Mark Gemmill's solution and it works well, but then stumbled across this slightly less verbose solution which removes the need for a fake module.

https://stackoverflow.com/a/15828369/1798234

So,

var provider;

beforeEach(module('app.config', function(theConfigProvider) {
    provider = theConfigProvider;
}))

it('tests the providers internal function', inject(function() {
    provider.mode('local')
    expect(provider.$get().mode).toBe('local');
}));


If your providers $get method has dependencies, you can either pass them in manually,

var provider;

beforeEach(module('app.config', function(theConfigProvider) {
    provider = theConfigProvider;
}))

it('tests the providers internal function', inject(function(dependency1, dependency2) {
    provider.mode('local')
    expect(provider.$get(dependency1, dependency2).mode).toBe('local');
}));


Or use the $injector to create a new instance,

var provider;

beforeEach(module('app.config', function(theConfigProvider) {
    provider = theConfigProvider;
}))

it('tests the providers internal function', inject(function($injector) {
    provider.mode('local')
    var service = $injector.invoke(provider);
    expect(service.mode).toBe('local');
}));


Both of the above would also allow you to reconfigure the provider for each individual it statement in a describe block. But if you only need to configure the provider once for multiple tests, you can do this,

var service;

beforeEach(module('app.config', function(theConfigProvider) {
    var provider = theConfigProvider;
    provider.mode('local');
}))

beforeEach(inject(function(theConfig){
    service = theConfig;
}));

it('tests the providers internal function', function() {
    expect(service.mode).toBe('local');
});

it('tests something else on service', function() {
    ...
});

Solution 3

@Stephane Catala's answer was particularly helpful, and I used his providerGetter to get exactly what I wanted. Being able to get the provider to do initialization and then the actual service to validate that things are working correctly with various settings was important. Example code:

    angular
        .module('test', [])
        .provider('info', info);

    function info() {
        var nfo = 'nothing';
        this.setInfo = function setInfo(s) { nfo = s; };
        this.$get = Info;

        function Info() {
            return { getInfo: function() {return nfo;} };
        }
    }

The Jasmine test spec:

    describe("provider test", function() {

        var infoProvider, info;

        function providerGetter(moduleName, providerName) {
            var provider;
            module(moduleName, 
                         [providerName, function(Provider) { provider = Provider; }]);
            return function() { inject(); return provider; }; // inject calls the above
        }

        beforeEach(function() {
            infoProvider = providerGetter('test', 'infoProvider')();
        });

        it('should return nothing if not set', function() {
            inject(function(_info_) { info = _info_; });
            expect(info.getInfo()).toEqual('nothing');
        });

        it('should return the info that was set', function() {
            infoProvider.setInfo('something');
            inject(function(_info_) { info = _info_; });
            expect(info.getInfo()).toEqual('something');
        });

    });

Solution 4

here is a little helper that properly encapsulates fetching providers, hence securing isolation between individual tests:

  /**
   * @description request a provider by name.
   *   IMPORTANT NOTE: 
   *   1) this function must be called before any calls to 'inject',
   *   because it itself calls 'module'.
   *   2) the returned function must be called after any calls to 'module',
   *   because it itself calls 'inject'.
   * @param {string} moduleName
   * @param {string} providerName
   * @returns {function} that returns the requested provider by calling 'inject'
   * usage examples:
    it('fetches a Provider in a "module" step and an "inject" step', 
        function() {
      // 'module' step, no calls to 'inject' before this
      var getProvider = 
        providerGetter('module.containing.provider', 'RequestedProvider');
      // 'inject' step, no calls to 'module' after this
      var requestedProvider = getProvider();
      // done!
      expect(requestedProvider.$get).toBeDefined();
    });
   * 
    it('also fetches a Provider in a single step', function() {
      var requestedProvider = 
        providerGetter('module.containing.provider', 'RequestedProvider')();

      expect(requestedProvider.$get).toBeDefined();
    });
   */
  function providerGetter(moduleName, providerName) {
    var provider;
    module(moduleName, 
           [providerName, function(Provider) { provider = Provider; }]);
    return function() { inject(); return provider; }; // inject calls the above
  }
  • the process of fetching the provider is fully encapsulated: no need for closure variables that reduce isolation between tests.
  • the process can be split in two steps, a 'module' step and an 'inject' step, which can be respectively grouped with other calls to 'module' and 'inject' within a unit test.
  • if splitting is not required, retrieving a provider can simply be done in a single command!

Solution 5

I only needed to test that some settings were being set correctly on the provider, so I used Angular DI to configure the provider when I was initialising the module via module().

I also had some issues with the provider not being found, after trying some of the above solutions, so that emphasised the need for an alternative approach.

After that, I added further tests that used the settings to check they were reflecting the use of new setting value.

describe("Service: My Service Provider", function () {
    var myService,
        DEFAULT_SETTING = 100,
        NEW_DEFAULT_SETTING = 500;

    beforeEach(function () {

        function configurationFn(myServiceProvider) {
            /* In this case, `myServiceProvider.defaultSetting` is an ES5 
             * property with only a getter. I have functions to explicitly 
             * set the property values.
             */
            expect(myServiceProvider.defaultSetting).to.equal(DEFAULT_SETTING);

            myServiceProvider.setDefaultSetting(NEW_DEFAULT_SETTING);

            expect(myServiceProvider.defaultSetting).to.equal(NEW_DEFAULT_SETTING);
        }

        module("app", [
            "app.MyServiceProvider",
            configurationFn
        ]);

        function injectionFn(_myService) {
            myService = _myService;
        }

        inject(["app.MyService", injectionFn]);
    });

    describe("#getMyDefaultSetting", function () {

        it("should test the new setting", function () {
            var result = myService.getMyDefaultSetting();

             expect(result).to.equal(NEW_DEFAULT_SETTING);
        });

    });

});
Share:
21,128
Maxim Grach
Author by

Maxim Grach

Updated on August 13, 2020

Comments

  • Maxim Grach
    Maxim Grach almost 4 years

    Does anyone have an example of how to unit test a provider?

    For example:

    config.js

    angular.module('app.config', [])
      .provider('config', function () {
        var config = {
              mode: 'distributed',
              api:  'path/to/api'
            };
    
        this.mode = function (type) {
          if (type) {
            config.isDistributedInstance = type === config.mode;
            config.isLocalInstance = !config.isDistributedInstance;
            config.mode = type;
            return this;
          } else {
            return config.mode;
          }
        };
    
        this.$get = function () {
          return config;
        };
      }]);
    

    app.js

    angular.module('app', ['app.config'])
      .config(['configProvider', function (configProvider) {
        configProvider.mode('local');
      }]);
    

    app.js is using in tests and I see already configured configProvider and I can test it as a service. But how can I test the ability to configure? Or it does not need at all?

  • zayquan
    zayquan over 9 years
    thank you for this post! I followed the guide found here and failed: github.com/angular/angular.js/issues/2274. The example above worked as expected. Thanks!
  • Enamul Hassan
    Enamul Hassan almost 9 years
    Others can delete their answer any time. So, your answer should not mention to any others answer.
  • ScottG
    ScottG almost 9 years
    only trying to give credit where credit is due. My answer is complete without the previous one, although it does go into more depth on providerGetter--but it took me a while to figure out how to test both the provider phase and the service phase (as was important for my real situation) so thought I would toss it in here--no other answer seems to test both setting and using.
  • Enamul Hassan
    Enamul Hassan almost 9 years
    That's good but credit would go to the place of credit. In this case, The place of the credit is giving up vote if you have the right to give up vote. otherwise, wait for the opportunities to give up vote. Your answer can be the better one, but that's not my topic (as I am involved here to review your question) and it is not the right place to give credit to other answer. For more info, join meta SO and know more about the community standard. @ScottG
  • Lee Goddard
    Lee Goddard over 8 years
    Perhaps rather than answering and crediting (which I admire), you could have edited and extended the answer you credit.
  • Dave McClelland
    Dave McClelland over 7 years
    When doing this, I had to change the fake module's declaration to pass in an empty array instead of an empty function. Likely due to a newer version of Angular.