How to test AngularJS custom provider

Unit TestingAngularjsJasmine

Unit Testing Problem Overview


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?

Unit Testing Solutions


Solution 1 - Unit Testing

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 - Unit Testing

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 - Unit Testing

@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 - Unit Testing

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 - Unit Testing

Personally I use this technique to mock providers coming from external libraries, which you could put in a helper file for all your tests. It can also work for a custom provider as in this question of course. The idea is to redefine the provider in his module before it is called by the app

describe('app', function() {
  beforeEach(module('app.config', function($provide) {
    $provide.provider('config', function() {
      var mode = jasmine.createSpy('config.mode');

      this.mode = mode;

      this.$get = function() {
        return {
          mode: mode
        };
      };
    });
  }));

  beforeEach(module('app'));

  describe('.config', function() {
    it('should call config.mode', inject(function(config) {
      expect(config.mode).toHaveBeenCalled();
    }));
  });
});

Solution 6 - Unit Testing

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);
        });

    });

});

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMaxim GrachView Question on Stackoverflow
Solution 1 - Unit TestingMark GemmillView Answer on Stackoverflow
Solution 2 - Unit TestingjamesView Answer on Stackoverflow
Solution 3 - Unit TestingScottGView Answer on Stackoverflow
Solution 4 - Unit TestingStephane CatalaView Answer on Stackoverflow
Solution 5 - Unit TestingGuillaumeView Answer on Stackoverflow
Solution 6 - Unit TestingAsh ClarkeView Answer on Stackoverflow