Injecting dependent services when unit testing AngularJS services

JavascriptUnit TestingAngularjsJasmineKarma Runner

Javascript Problem Overview


I'm testing service A, but service A depends on service B (i.e. service B is injected into service A).

I've seen this question but my case is a bit different because in my opinion it makes more sense to mock service B instead of injecting an actual instance of service B. I'd mock it with a jasmine spy.

Here's a sample test:

describe("Sample Test Suite", function() {

  beforeEach(function() {

    module('moduleThatContainsServiceA');

    inject([
      'serviceA', function(service) {
        this.service = service;
      }
    ]);

  });

  it('can create an instance of the service', function() {
    expect(this.service).toBeDefined();
  });
});

The error I get is:

> Error: Unknown provider: serviceBProvider

How could I do something like this?

Javascript Solutions


Solution 1 - Javascript

Actually in AngularJS Dependency Injection uses the 'last wins' rule. So you can define your service in your test just after including your module and dependencies, and then when service A that you're testing will request service B using DI, AngularJS will give mocked version of service B.

This is often is done by defining new module like MyAppMocks, putting mocked services/values there and then just adding this module as dependency.

Kind of (schematically):

beforeEach(function() {
  angular.module('MyAppMocks',[]).service('B', ...));
  angular.module('Test',['MyApp','MyAppMocks']);
  ...

Solution 2 - Javascript

I was doing this in CoffeeScript and found an extra gotcha. (Also, I found the code on this page to be confusingly terse.) Here's a complete working example:

describe 'serviceA', ->
   mockServiceB = {}

   beforeEach module 'myApp' # (or just 'myApp.services')

   beforeEach ->
      angular.mock.module ($provide) ->
         $provide.value 'serviceB', mockServiceB
         null

   serviceA = null
   beforeEach inject ($injector) ->
      serviceA = $injector.get 'serviceA'

   it 'should work', ->
      expect( true ).toBe( true )
      #serviceA.doStuff()

Without explicitly returning null after $provide.value, I kept getting Error: Argument 'fn' is not a function, got Object. I found the answer in this Google Groups thread.

Solution 3 - Javascript

The Valentyn solution worked for me, but there is another alternative.

beforeEach(function () {

    angular.mock.module("moduleThatContainsServiceA", function ($provide) {
                $provide.value('B', ...);
            });
});

Then when AngularJS service A request the Service B by Dependency Injection, your mock of Service B will be provided instead of the Service B from moduleThatContainsServiceA.

This way you don't need to create an additional angular module just to mock a Service.

Solution 4 - Javascript

I find the simplest method is just to inject service B and mock it. e.g. Service car depends on service Engine. Now we need to mock Engine when testing Car:

describe('Testing a car', function() {
	  var testEngine;

  beforeEach(module('plunker'));
  beforeEach(inject(function(engine){
	testEngine = engine;
  }));

  it('should drive slow with a slow engine', inject(function(car) {
	spyOn(testEngine, 'speed').andReturn('slow');
	expect(car.drive()).toEqual('Driving: slow');
  }));
});

Reference: https://github.com/angular/angular.js/issues/1635

Solution 5 - Javascript

This is what worked for me. The key is defining a real module to be mocked. Calling angular.mock.module makes the real module mockable and allows things to be connected.

	beforeEach( ->
		@weather_service_url = '/weather_service_url'
		@weather_provider_url = '/weather_provider_url'
		@weather_provider_image = "test.jpeg"
		@http_ret = 'http_works'
		module = angular.module('mockModule',[])
		module.value('weather_service_url', @weather_service_url)
		module.value('weather_provider_url', @weather_provider_url)
		module.value('weather_provider_image', @weather_provider_image)
		module.service('weather_bug_service', services.WeatherBugService)

		angular.mock.module('mockModule')

		inject( ($httpBackend,weather_bug_service) =>
			@$httpBackend = $httpBackend
			@$httpBackend.when('GET', @weather_service_url).respond(@http_ret)
			@subject = weather_bug_service
		)
	)

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
QuestionRoy TrueloveView Question on Stackoverflow
Solution 1 - JavascriptValentyn ShybanovView Answer on Stackoverflow
Solution 2 - JavascriptjabView Answer on Stackoverflow
Solution 3 - JavascriptRichard KellerView Answer on Stackoverflow
Solution 4 - Javascriptgm2008View Answer on Stackoverflow
Solution 5 - JavascriptNickView Answer on Stackoverflow