Dependency Injection (DI) is a fundamental concept in AngularJS, enabling modular and testable applications. However, incorrect DI syntax often leads to unit test failures in Jasmine, displaying errors like:
- “[$injector:unpr] Unknown provider: myServiceProvider”
- “Error: Expected function but got undefined”
- “Cannot read property ‘method’ of undefined”
This guide explores common DI mistakes and how to fix them to ensure successful unit testing.
1. Understanding AngularJS Dependency Injection (DI)
AngularJS uses three DI syntax styles:
1️⃣ Inline Array Annotation (Recommended)
This syntax ensures proper dependency injection, especially after minification:
angular.module('myApp').controller('MyController', ['$scope', 'myService', function($scope, myService) {
$scope.data = myService.getData();
}]);
Why it works:
- Keeps dependency names intact after minification.
- Ensures the correct order of injected services.
2️⃣ $inject Property (Also Safe for Minification)
Another safe way to inject dependencies:
function MyController($scope, myService) {
$scope.data = myService.getData();
}
MyController.$inject = ['$scope', 'myService'];
angular.module('myApp').controller('MyController', MyController);
Why it works:
- Preserves correct dependencies even after code minification.
3️⃣ Implicit Injection ( Causes Issues in Tests and Minified Code)
This approach fails in minified code and tests because parameter names are lost:
angular.module('myApp').controller('MyController', function($scope, myService) {
$scope.data = myService.getData();
});
Why it fails in unit tests:
- Minification renames variables (
$scope→a,myService→b), breaking the DI mechanism. - Jasmine unit tests may not properly inject dependencies.
2. Common DI Mistakes and Fixes in Unit Tests
Mistake 1: Forgetting to Inject Dependencies in Tests
Incorrect:
describe('MyController', function() {
beforeEach(module('myApp'));
it('should call getData from myService', function() {
var $controller = angular.injector(['ng', 'myApp']).get('$controller');
var controller = $controller('MyController'); // Error: myService is undefined
expect(controller.data).toBeDefined();
});
});
Fix:
describe('MyController', function() {
var $controller, $scope, myServiceMock;
beforeEach(module('myApp'));
beforeEach(inject(function(_$controller_, _$rootScope_, _myService_) {
$controller = _$controller_;
$scope = _$rootScope_.$new();
myServiceMock = _myService_;
}));
it('should call getData from myService', function() {
var controller = $controller('MyController', { $scope: $scope, myService: myServiceMock });
expect($scope.data).toBeDefined();
});
});
What it fixes:
- Ensures dependencies (
$controller,$scope,myServiceMock) are injected before running tests.
Mistake 2: Not Using $inject in a Standalone Function
Incorrect:
function MyController($scope, myService) { // Minification will break this
$scope.data = myService.getData();
}
angular.module('myApp').controller('MyController', MyController);
Fix:
function MyController($scope, myService) {
$scope.data = myService.getData();
}
MyController.$inject = ['$scope', 'myService'];
angular.module('myApp').controller('MyController', MyController);
Why it works:
- Prevents DI issues in tests and production minification.
Mistake 3: Forgetting to Mock Services in Unit Tests
Incorrect:
describe('MyController', function() {
var $controller, $scope;
beforeEach(module('myApp'));
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$scope = _$rootScope_.$new();
}));
it('should call getData from myService', function() {
var controller = $controller('MyController', { $scope: $scope }); // myService is missing
expect($scope.data).toBeDefined();
});
});
Fix (Mock the Service):
describe('MyController', function() {
var $controller, $scope, myServiceMock;
beforeEach(module('myApp'));
beforeEach(module(function($provide) {
myServiceMock = {
getData: jasmine.createSpy('getData').and.returnValue('mocked data')
};
$provide.value('myService', myServiceMock);
}));
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$scope = _$rootScope_.$new();
}));
it('should call getData from myService', function() {
var controller = $controller('MyController', { $scope: $scope, myService: myServiceMock });
expect(myServiceMock.getData).toHaveBeenCalled();
expect($scope.data).toBe('mocked data');
});
});
Why it works:
- Mocks
myServiceso that the unit test doesn’t depend on the real service. - Ensures isolation in testing.
3. Debugging Dependency Injection Errors
If you still get DI errors, try debugging with angular.injector() in DevTools:
angular.injector(['ng', 'myApp']).annotate(MyController);
It returns:
["$scope", "myService"]
Confirms the correct dependencies are being injected.
4. Summary
| Issue | Fix |
|---|---|
| Unknown provider: myServiceProvider | Use ['$scope', 'myService', function($scope, myService) {...}] |
| Minified code breaks DI | Use $inject = ['$scope', 'myService']; |
| Test fails due to missing service | Use $provide.value('myService', mockService); |
| Service methods not being called in tests | Use spyOn(myService, 'method').and.returnValue(mockValue); |
| Cannot read property of undefined | Ensure dependencies are injected in $controller() |
