Why is $provide only available in the 'angular.mock.module' function, and $q only available in the 'angular.mock.inject' function?

25,332

Solution 1

You can't use $provide within the inject function because the former registers providers for the latter to use. Take a look:

describe('...', function() {
    beforeEach(function() {
        module(function($provide) {
            $provide.constant('someValue', 'foobar');
        });

        inject(function(someValue) {
            var value = someValue; // will be 'foobar';
        });
    });
});

You can though write your test this way:

describe('...', function() {
    var serviceMock;

    beforeEach(function() {
        serviceMock = {
           someMethod: function() { ... }
        };

        module(function($provide) {
            $provide.value('service', serviceMock);
        });

        inject(function(service) {
            ...                         
        });
    });
});

In fact, you don't even need to implement the mocked service before injecting it with $provide:

beforeEach(function() {
    serviceMock = {};

    module(function($provide) {
        $provide.value('service', serviceMock);
    });

    inject(function(service) {
        ...                         
    });
});

it('tests something', function() {
    // Arrange
    serviceMock.someMethod = function() { ... }

    // Act
    // does something

    // Assert
    expect(...).toBe(...);
});

Here's a Plunker script illustrating mostly of the above.

Solution 2

This worked for me when I had to wrap a service which used $q and seems quite clean:

var _ServiceToTest_;
beforeEach(function () {
    module('module.being.tested');
    module(function ($provide) {
        $provide.factory('ServiceToMock', function ($q, $rootScope) {
            var service = ...;
            // use $q et al to heart's content
            return service;
        });
    });
    inject(function (_ServiceToTest_) {
        ServiceToTest = _ServiceToTest_;
    });
});

it('...', function () { /* code using ServiceToTest */ });

The trick was to use $provide.factory instead of $provide.value.

Share:
25,332
Holf
Author by

Holf

Updated on July 25, 2022

Comments

  • Holf
    Holf almost 2 years

    I am mocking out a service for an AngularJS Unit Test. I'm using the $provide service to replace the 'real' service with the mocked out one (a plunker script of this is available):

    describe('My Controller', function () {
    
        var $scope;
        var $provide;
    
        beforeEach(angular.mock.module('myApp'));
    
        beforeEach(angular.mock.module(function (_$provide_) {
    
            $provide = _$provide_;
    
        }));
    
        beforeEach(angular.mock.inject(function($rootScope, $controller, $q){
    
            var mockMyService = {
                getAll : function() {
                    var deferred = $q.defer();
                    deferred.resolve([
                { itemText: "Foo" },
                { itemText: "Bar" }
                    ]);
    
                    return deferred.promise;
                }
            };
    
            $provide.value('myService', mockMyService);
    
            $scope = $rootScope.$new();
    
            $controller('MyCtrl', { $scope: $scope });
    
            $rootScope.$apply();
    
        }));
    
        it('Has two items defined', function () {
            expect($scope.items.length).toEqual(2);
        });
    });
    

    This works just fine. However, I don't like the fact that I am using an angular.mock.module function simply to give a reference to the $provide service which is then used in the angular.mock.inject function below. But if I add $provide as a parameter to the angular.mock.inject function directly instead, I get an 'unknown provider' error.

    It occurs to me that I could put all the mocking code in the angular.mock.module function. But then I have a similar issue with the $q reference, which I need as my mocked service has to return a promise.

    In other words, if I add a $q parameter to the angular.mock.module function then I also get an 'unknown provider' error.

    Is there a way to simplify this? Obviously what I have works but it doesn't feel quite right, somehow. I feel that I lack understanding of why some providers are available in inject functions and others are available in module functions.

  • Holf
    Holf over 10 years
    This is very nice and it works... as long as your not using a Promise. Here is a plunk showing my original example: plnkr.co/edit/1Gbr1N?p=preview And here is a fork updated with the technique you suggest: plnkr.co/edit/ptAWcb?p=preview The mock service is not getting assigned; it looks as though something is interfering with the timing.
  • Holf
    Holf over 10 years
    I wonder why I can't get hold of the '$q' library from the 'module' function? If I could, then I would have everything I need to create the mock service there.
  • Michael Benford
    Michael Benford over 10 years
    @Holf You can use a promise, but you need to move things around a little bit first. Check out this updated Plunker script.
  • Michael Benford
    Michael Benford over 10 years
    @Holf You can't get a instance of the $q service within the module function because Angular doesn't inject any service into it. The proper way to do that is by using the inject function. That's by design, as far as I can tell.
  • Holf
    Holf over 10 years
    Thanks, this is all really cool and it now makes a lot more sense.
  • FutuToad
    FutuToad over 10 years
    @MichaelBenford great answer! btw I was wondering what was the diff between angular.mock.module vs angular.module
  • Michael Benford
    Michael Benford over 10 years
    @FutuToad angular.module is used to define a module while angular.mock.module is used to load an existing module into memory so tests can get access to its content (services, factories, directives etc). mock.module is also used to configure the injector so you can provide mock objects for any injectable service. Take a look at the Angular docs for more information.
  • FutuToad
    FutuToad over 10 years
    @MichaelBenford thx, but in my tests: beforeEach(angular.mock.module('app')); makes all my tests pass just as when I do: beforeEach(module('app'));
  • Michael Benford
    Michael Benford over 10 years
    @FutuToad module is a global alias for angular.mock.module, as said in the docs: This function is also published on window for easy access..
  • ravishi
    ravishi almost 9 years
    I studied the differences between Holf's plnkr and @michael-benford's plnkr to figure out why Michael's worked and why Holf's didn't. At first, I thought it was due to the arrangement order of Michael's code but it turns out there was a simple error in Holf's. Holf's just needed to initialize mockMyService to an empty object and then not overwrite it when defining the getAll mocked method. That way the provided mock service gets used. Updated Holf's plnkr.