I'm fairly new to Angular and have reviewed all the similarly related questions on Stack Overflow but none have helped me. I believe I have everything set up correctly but am still getting an 'Unknown Provider' error when attempting to inject a service into a unit test. I have laid out my code below - hopefully someone can spot an obvious error!
I define my modules in a seperate .js file like this:
angular.module('dashboard.services', []);
angular.module('dashboard.controllers', []);
Here is where I define a service called EventingService (with logic removed for brevity):
angular.module('dashboard.services').factory("EventingService", [function () {
//Service logic here
}]);
Here is my controller that uses the EventingService (this all works fine at runtime):
angular.module('dashboard.controllers')
.controller("Browse", ['$scope', "EventingService", function ($scope, eventing) {
//Controller logic here
}]);
Here is my unit test - its the line where I attempt to inject the EventingService that causes an error when I run the unit test:
describe('Browse Controller Tests.', function () {
beforeEach(function () {
module('dashboard.services');
module('dashboard.controllers');
});
var controller, scope, eventingService;
beforeEach(inject(function ($controller, $rootScope, EventingService) {
scope = $rootScope.$new();
eventingService = EventingService
controller = $controller('Browse', {
$scope: scope,
eventing: eventingService
});
}));
it('Expect True to be True', function () {
expect(true).toBe(true);
});
});
When I run the test I get this error:
Error: Unknown provider: EventingServiceProvider <- EventingService
I have ensured that my jasmine specrunner.html file has all the necessary source files (this is an Asp.Net MVC project):
<!-- Include source files here... -->
@Scripts.Render("~/bundles/jquery")
<script type="text/javascript" src="@Url.Content("~/Scripts/angular.js")"></script>
<script type="text/javascript" src="@Url.Content("~/Scripts/angular-mocks.js")"></script>
<script type="text/javascript" src="@Url.Content("~/App/scripts/app.js")"></script> <!-- Angular modules defined in here -->
<script type="text/javascript" src="@Url.Content("~/App/scripts/services/eventing.js")"></script> <!-- My Eventing service defined here -->
<script type="text/javascript" src="@Url.Content("~/App/scripts/controllers/browse.js")"></script> <!-- My Browse controller defined here -->
<!-- Include spec files here... -->
<script type="text/javascript" src="@Url.Content("~/App/tests/browse.js")"></script> <!-- The actual unit test here -->
I just can not fathom out why Angular is throwing this error complaining about my EventingService. My controller works fine at runtime - its just when I try to test it that I am getting an error so I am curious as to whether I have screwed something up with the mocking/injection.
The Angular help on testing is rubbish so I am stumped at present - any help or suggestions anyone can give would be very appreciated. Thanks.
I just ran into this and solved it by switching to getting the service using the $injector explicitly:
var EventingService, $rootScope;
beforeEach(inject(function($injector) {
EventingService = $injector.get('EventingService');
$rootScope = $injector.get('$rootScope');
}));
I wish I could tell you why this works and why the simple
beforeEach(inject(function(EventingService) { .... }));
does not, but I don't have the time to investigate the internals. Always best to use one coding style and stick to it.
This style is better in that the name of the variable that you use in your tests is the correct name of the Service. But it is a bit verbose.
There is another angular magic feature that uses strange variable names like $rootScope but I don't like the hacky look of that.
Note that the most of the time people get this error because they didn't include the modules:
beforeEach(module('capsuling'));
beforeEach(module('capsuling.capsules.services'));
If your controllers (defined under dashboard.controllers
module) depend on some services which are enclosed in different module (dashboard.services
) than you need to reference the dependency modules in your module signature:
angular.module('dashboard.services', []);
angular.module('dashboard.controllers', ['dashboard.services']);
Have you tried defining an additional module that depends on your other aggregated modules like so:
angular.module( 'dashboard', [ 'dashboard.services', 'dashboard.controllers' ] )
So you can in the beforeEach specify the one module that has both submodules defined in it like so:
describe('Browse Controller Tests.', function () {
beforeEach(function () {
module('dashboard');
});
var controller, scope, eventingService;
beforeEach(inject(function ($controller, $rootScope, EventingService) {
scope = $rootScope.$new();
eventingService = EventingService
controller = $controller('Browse', {
$scope: scope,
eventing: eventingService
});
}));
it('Expect True to be True', function () {
expect(true).toBe(true);
});
});
The solution for it is inject the module that contains the service before all:
describe('LeftSideController', function() {
/**
* Load the controller's module
*/
beforeEach(function(){
module('app.controllers');
module('app.factories');
});
beforeEach(inject( function($rootScope, $controller) {
scope = $rootScope.$new();
LeftSideController = $controller('LeftSideController', {
$scope: scope
});
}));
it('should exist', function() {
expect(LeftSideController).toBeTruthy();
});
it('has a scope variable defined', function() {
expect(scope).toBeDefined();
});
});
Because I have the same situation:
$ karma start client/test/my.conf.js
INFO [karma]: Karma v0.12.37 server started at http://localhost:9876/
INFO [launcher]: Starting browser PhantomJS
INFO [PhantomJS 1.9.8 (Linux 0.0.0)]: Connected on socket F_J4L4aTrFgwgtNUd_J6 with id 15322232
PhantomJS 1.9.8 (Linux 0.0.0) LeftSideController should exist FAILED
Error: [$injector:unpr] Unknown provider: PhoneRetrieverProvider <- PhoneRetriever
http://errors.angularjs.org/1.4.3/$injector/unpr?p0=PhoneRetrieverProvider%20%3C-%20PhoneRetriever
I just have a service:
var app = angular.module('app.factories', []);
app.factory('PhoneRetriever', ['$http', '$q', function($http, $q){
// variables
var PhoneRetriever = new Object();
PhoneRetriever.getPhones = function(query) {
var deferred = $q.defer();
var phone = '+521'+query;
var request = "/api/clients?filter[where][cellphone]=" + encodeURIComponent(phone);
$http.get(request).success(function(data){
deferred.resolve(data);
});
return deferred.promise;
}
return PhoneRetriever;
}]);
And a controller that use the previous service:
var app = angular.module('app.controllers',
['toaster', 'ngAnimate']);
////////////////////////////////////////////////////
// controller for the numneric keyboard
app.controller('LeftSideController', ['$scope','$http','PhoneRetriever',
function($scope,$http,PhoneRetriever){...}]);