Injecting dependent services when unit testing AngularJS services

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?


Solution 1:

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:

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:

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:

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:

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