How do I test an AngularJS service with Jasmine?
Solution 1
The problem is that the factory method, that instantiate the service, is not called in the example above (only creating the module doesn't instantiate the service).
In order to the service to be instantiated angular.injector has to be called with the module where our service is defined. Then, we can ask to the new injector object for the service and its only then when the service is finally instantiated.
Something like this works:
describe('myService test', function(){
describe('when I call myService.one', function(){
it('returns 1', function(){
var $injector = angular.injector([ 'myModule' ]);
var myService = $injector.get( 'myService' );
expect( myService.one ).toEqual(1);
})
})
});
Another way would be passing the service to a function using 'invoke':
describe('myService test', function(){
describe('when I call myService.one', function(){
it('returns 1', function(){
myTestFunction = function(aService){
expect( aService.one ).toEqual(1);
}
//we only need the following line if the name of the
//parameter in myTestFunction is not 'myService' or if
//the code is going to be minify.
myTestFunction.$inject = [ 'myService' ];
var myInjector = angular.injector([ 'myModule' ]);
myInjector.invoke( myTestFunction );
})
})
});
And, finally, the 'proper' way to do it is using 'inject' and 'module' in a 'beforeEach' jasmine block. When doing it we have to realize that the 'inject' function it's not in the standard angularjs package, but in the ngMock module and that it only works with jasmine.
describe('myService test', function(){
describe('when I call myService.one', function(){
beforeEach(module('myModule'));
it('returns 1', inject(function(myService){ //parameter name = service name
expect( myService.one ).toEqual(1);
}))
})
});
Solution 2
While the answer above probably works just fine (I haven't tried it :) ), I often have a lot more tests to run so I don't inject in tests themselves. I'll group it() cases into describe blocks and run my injection in a beforeEach() or beforeAll() in each describe block.
Robert is also correct in that he says you must use the Angular $injector to make the tests aware of the service or factory. Angular uses this injector itself in your applications, too, to tell the application what is available. However, it can be called in more than one place, and it can also be called implicitly instead of explicitly. You'll notice in my example spec test file below, the beforeEach() block implicitly calls injector to make things available to be assigned inside of the tests.
Going back to grouping things and using before-blocks, here's a small example. I'm making a Cat Service and I want to test it, so my simple setup to write and test the Service would look like this:
app.js
var catsApp = angular.module('catsApp', ['ngMockE2E']);
angular.module('catsApp.mocks', [])
.value('StaticCatsData', function() {
return [{
id: 1,
title: "Commando",
name: "Kitty MeowMeow",
score: 123
}, {
id: 2,
title: "Raw Deal",
name: "Basketpaws",
score: 17
}, {
id: 3,
title: "Predator",
name: "Noseboops",
score: 184
}];
});
catsApp.factory('LoggingService', ['$log', function($log) {
// Private Helper: Object or String or what passed
// for logging? Let's make it String-readable...
function _parseStuffIntoMessage(stuff) {
var message = "";
if (typeof stuff !== "string") {
message = JSON.stringify(stuff)
} else {
message = stuff;
}
return message;
}
/**
* @summary
* Write a log statement for debug or informational purposes.
*/
var write = function(stuff) {
var log_msg = _parseStuffIntoMessage(stuff);
$log.log(log_msg);
}
/**
* @summary
* Write's an error out to the console.
*/
var error = function(stuff) {
var err_msg = _parseStuffIntoMessage(stuff);
$log.error(err_msg);
}
return {
error: error,
write: write
};
}])
catsApp.factory('CatsService', ['$http', 'LoggingService', function($http, Logging) {
/*
response:
data, status, headers, config, statusText
*/
var Success_Callback = function(response) {
Logging.write("CatsService::getAllCats()::Success!");
return {"status": status, "data": data};
}
var Error_Callback = function(response) {
Logging.error("CatsService::getAllCats()::Error!");
return {"status": status, "data": data};
}
var allCats = function() {
console.log('# Cats.allCats()');
return $http.get('/cats')
.then(Success_Callback, Error_Callback);
}
return {
getAllCats: allCats
};
}]);
var CatsController = function(Cats, $scope) {
var vm = this;
vm.cats = [];
// ========================
/**
* @summary
* Initializes the controller.
*/
vm.activate = function() {
console.log('* CatsCtrl.activate()!');
// Get ALL the cats!
Cats.getAllCats().then(
function(litter) {
console.log('> ', litter);
vm.cats = litter;
console.log('>>> ', vm.cats);
}
);
}
vm.activate();
}
CatsController.$inject = ['CatsService', '$scope'];
catsApp.controller('CatsCtrl', CatsController);
Spec: Cats Controller
'use strict';
describe('Unit Tests: Cats Controller', function() {
var $scope, $q, deferred, $controller, $rootScope, catsCtrl, mockCatsData, createCatsCtrl;
beforeEach(module('catsApp'));
beforeEach(module('catsApp.mocks'));
var catsServiceMock;
beforeEach(inject(function(_$q_, _$controller_, $injector, StaticCatsData) {
$q = _$q_;
$controller = _$controller_;
deferred = $q.defer();
mockCatsData = StaticCatsData();
// ToDo:
// Put catsServiceMock inside of module "catsApp.mocks" ?
catsServiceMock = {
getAllCats: function() {
// Just give back the data we expect.
deferred.resolve(mockCatsData);
// Mock the Promise, too, so it can run
// and call .then() as expected
return deferred.promise;
}
};
}));
// Controller MOCK
var createCatsController;
// beforeEach(inject(function (_$rootScope_, $controller, FakeCatsService) {
beforeEach(inject(function (_$rootScope_, $controller, CatsService) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
createCatsController = function() {
return $controller('CatsCtrl', {
'$scope': $scope,
CatsService: catsServiceMock
});
};
}));
// ==========================
it('should have NO cats loaded at first', function() {
catsCtrl = createCatsController();
expect(catsCtrl.cats).toBeDefined();
expect(catsCtrl.cats.length).toEqual(0);
});
it('should call "activate()" on load, but only once', function() {
catsCtrl = createCatsController();
spyOn(catsCtrl, 'activate').and.returnValue(mockCatsData);
// *** For some reason, Auto-Executing init functions
// aren't working for me in Plunkr?
// I have to call it once manually instead of relying on
// $scope creation to do it... Sorry, not sure why.
catsCtrl.activate();
$rootScope.$digest(); // ELSE ...then() does NOT resolve.
expect(catsCtrl.activate).toBeDefined();
expect(catsCtrl.activate).toHaveBeenCalled();
expect(catsCtrl.activate.calls.count()).toEqual(1);
// Test/Expect additional conditions for
// "Yes, the controller was activated right!"
// (A) - there is be cats
expect(catsCtrl.cats.length).toBeGreaterThan(0);
});
// (B) - there is be cats SUCH THAT
// can haz these properties...
it('each cat will have a NAME, TITLE and SCORE', function() {
catsCtrl = createCatsController();
spyOn(catsCtrl, 'activate').and.returnValue(mockCatsData);
// *** and again...
catsCtrl.activate();
$rootScope.$digest(); // ELSE ...then() does NOT resolve.
var names = _.map(catsCtrl.cats, function(cat) { return cat.name; })
var titles = _.map(catsCtrl.cats, function(cat) { return cat.title; })
var scores = _.map(catsCtrl.cats, function(cat) { return cat.score; })
expect(names.length).toEqual(3);
expect(titles.length).toEqual(3);
expect(scores.length).toEqual(3);
});
});
Spec: Cats Service
'use strict';
describe('Unit Tests: Cats Service', function() {
var $scope, $rootScope, $log, cats, logging, $httpBackend, mockCatsData;
beforeEach(module('catsApp'));
beforeEach(module('catsApp.mocks'));
describe('has a method: getAllCats() that', function() {
beforeEach(inject(function($q, _$rootScope_, _$httpBackend_, _$log_, $injector, StaticCatsData) {
cats = $injector.get('CatsService');
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
// We don't want to test the resolving of *actual data*
// in a unit test.
// The "proper" place for that is in Integration Test, which
// is basically a unit test that is less mocked - you test
// the endpoints and responses and APIs instead of the
// specific service behaviors.
mockCatsData = StaticCatsData();
// For handling Promises and deferrals in our Service calls...
var deferred = $q.defer();
deferred.resolve(mockCatsData); // always resolved, you can do it from your spec
// jasmine 2.0
// Spy + Promise Mocking
// spyOn(obj, 'method'), (assumes obj.method is a function)
spyOn(cats, 'getAllCats').and.returnValue(deferred.promise);
/*
To mock $http as a dependency, use $httpBackend to
setup HTTP calls and expectations.
*/
$httpBackend.whenGET('/cats').respond(200, mockCatsData);
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
})
it(' exists/is defined', function() {
expect( cats.getAllCats ).toBeDefined();
expect( typeof cats.getAllCats ).toEqual("function");
});
it(' returns an array of Cats, where each cat has a NAME, TITLE and SCORE', function() {
cats.getAllCats().then(function(data) {
var names = _.map(data, function(cat) { return cat.name; })
var titles = _.map(data, function(cat) { return cat.title; })
var scores = _.map(data, function(cat) { return cat.score; })
expect(names.length).toEqual(3);
expect(titles.length).toEqual(3);
expect(scores.length).toEqual(3);
})
});
})
describe('has a method: getAllCats() that also logs', function() {
var cats, $log, logging;
beforeEach(inject(
function(_$log_, $injector) {
cats = $injector.get('CatsService');
$log = _$log_;
logging = $injector.get('LoggingService');
spyOn(cats, 'getAllCats').and.callThrough();
}
))
it('that on SUCCESS, $logs to the console a success message', function() {
cats.getAllCats().then(function(data) {
expect(logging.write).toHaveBeenCalled();
expect( $log.log.logs ).toContain(["CatsService::getAllCats()::Success!"]);
})
});
})
});
EDIT Based on some of the comments, I've updated my answer to be slightly more complex, and I've also made up a Plunkr demonstrating Unit Testing. Specifically, one of the comments mentioned "What if a Controller's Service has itself a simple dependency, such as $log?" - which is included in the example with test cases. Hope it helps! Test or Hack the Planet!!!
https://embed.plnkr.co/aSPHnr/
Related videos on Youtube
Robert
Updated on May 08, 2020Comments
-
Robert about 4 years
(There is a related question here: Jasmine test does not see AngularJS module)
I just want to test a service without bootstrapping Angular.
I have look at some examples and the tutorial but I am not going anywhere.
I have just three files:
myService.js: where I define an AngularJS service
test_myService.js: where I define a Jasmine test for the service.
specRunner.html: a HTML file with the normal jasmine configuration and where I import the previous two other files and the Jasmine, Angularjs and angular-mocks.js.
This is the code for the service (that works as expected when I am not testing):
var myModule = angular.module('myModule', []); myModule.factory('myService', function(){ var serviceImplementation = {}; serviceImplementation.one = 1; serviceImplementation.two = 2; serviceImplementation.three = 3; return serviceImplementation });
As I am trying to test the service in isolation, I should be able to access it and check their methods. My question is: how can I inject the service in my test without bootstrapping AngularJS?
For instance, how can I test the value returned for a method of the service with Jasmine like this:
describe('myService test', function(){ describe('when I call myService.one', function(){ it('returns 1', function(){ myModule = angular.module('myModule'); //something is missing here.. expect( myService.one ).toEqual(1); }) }) });
-
Roy Truelove about 11 yearsWould love to see an example of when your service has dependencies of its own (e.g. $log)
-
Robert about 11 years@RoyTruelove : The AngularJS injector should be able to take care of any dependencies by itself. After all, that is the point of using injectors. I assume that your dependencies are properly defined in your service definition. I.E: angular.module('myModule'). factory('myService', function( $log, $rootScope ) {} );
-
Roy Truelove about 11 yearsSorry, I was actually looking for something like this: stackoverflow.com/q/16565531/295797
-
CSS almost 9 yearsIs there a good way to inject the service in a
beforeEach
in the case of many...many...many tests being necessary for the service? Testing a data model (service) and it holds a ton of global variables. Thanks, C§ -
Michel over 8 years5 minutes before giving up and go home, you save my day!
-
Lee Goddard over 8 yearsYou do not say why (3) is the 'proper way'
-
Robert over 8 years@LeeGee I think we can call it the 'proper' way because it use the ngMock AngularJS module that it's there specifically for testing purposes.
-
Lee Goddard over 8 yearsHaving been doing this since writing that comment, I can agree. But it wasn't obvious when I started out.
-
Bartek S about 8 yearsquestion is about testing service, not controller.
-
tigeruppercut over 7 years@CSS yes; you can nest a call to inject() inside a beforeEach(). So in this example it would be:
beforeEach(function() { module('myModule')); inject(function(myService) { myService = myService; }); });
-
RoboBear about 7 years@RoyTruelove I know it's a little late, but I've updated my answer for Angular 1.4.14 with an example of Unit Testing a Service that has itself a decency like $log. Hope it helps!