mock angular service/promise in a karma/jasmine test - angularjs

I'm trying to write a karma/jasmine test and I would like some explanations about how mocks are working on a service which is returning a promise. I explain my situation :
I have a controller in which I do the following call :
mapService.getMapByUuid(mapUUID, isEditor).then(function(datas){
fillMapDatas(datas);
});
function fillMapDatas(datas){
if($scope.elements === undefined){
$scope.elements = [];
}
//Here while debugging my unit test, 'datas' contain the promise javascript object instead //of my real reponse.
debugger;
var allOfThem = _.union($scope.elements, datas.elements);
...
Here is how my service is :
(function () {
'use strict';
var serviceId = 'mapService';
angular.module('onmap.map-module.services').factory(serviceId, [
'$resource',
'appContext',
'restHello',
'restMap',
serviceFunc]);
function serviceFunc($resource, appContext, restHello, restMap) {
var Maps = $resource(appContext+restMap, {uuid: '#uuid', editor: '#editor'});
return{
getMapByUuid: function (uuid, modeEditor) {
var maps = Maps.get({'uuid' : uuid, 'editor': modeEditor});
return maps.$promise;
}
};
}
})();
And finally, here is my unit test :
describe('Map controller', function() {
var $scope, $rootScope, $httpBackend, $timeout, createController, MapService, $resource;
beforeEach(module('onmapApp'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
var $controller = $injector.get('$controller');
createController = function() {
return $controller('maps.ctrl', {
'$scope': $scope
});
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
var response = {"elements":[1,2,3]};
it('should allow user to get a map', function() {
var controller = createController();
$httpBackend.expect('GET', '/onmap/rest/map/MY-UUID?editor=true')
.respond({
"success": response
});
// hope to call /onmap/rest/map/MY-UUID?editor=true url and hope to have response as the fillMapDatas parameter
$scope.getMapByUUID('MY-UUID', true);
$httpBackend.flush();
});
});
What I really want to do is to have my response object ( {"elements:...}) as the datas parameter of the fillMapDatas function. I don't understand how to mock all the service things (service, promise, then)

So you want to test, if your service responses as expected? Then, this is something you would rather test on the service. Unit test promise based methods could look like this:
var mapService, $httpBackend, $q, $rootScope;
beforeEach(inject(function (_mapService_, _$httpBackend_, _$q_, _$rootScope_) {
mapService = mapService;
$httpBackend = _$httpBackend_;
$q = _$q_;
$rootScope = _$rootScope_;
// expect the actual request
$httpBackend.expect('GET', '/onmap/rest/map/uuid?editor=true');
// react on that request
$httpBackend.whenGET('/onmap/rest/map/uuid?editor=true').respond({
success: {
elements: [1, 2, 3]
}
});
}));
As you can see, you don't need to use $injector, since you can inject your needed services directly. If you wanna use the correct service names throughout your tests, you can inject them with prefixed and suffixed "_", inject() is smart enough to recognise which service you mean. We also setup the $httpBackend mock for each it() spec. And we set up $q and $rootScope for later processing.
Here's how you could test that your service method returns a promise:
it('should return a promise', function () {
expect(mapService.getMapUuid('uuid', true).then).toBeDefined();
});
Since a promise always has a .then() method, we can check for this property to see if it's a promise or not (of course, other objects could have this method too).
Next you can test of the promise you get resolves with the proper value. You can do that setting up a deferred that you explicitly resolve.
it('should resolve with [something]', function () {
var data;
// set up a deferred
var deferred = $q.defer();
// get promise reference
var promise = deferred.promise;
// set up promise resolve callback
promise.then(function (response) {
data = response.success;
});
mapService.getMapUuid('uuid', true).then(function(response) {
// resolve our deferred with the response when it returns
deferred.resolve(response);
});
// force `$digest` to resolve/reject deferreds
$rootScope.$digest();
// make your actual test
expect(data).toEqual([something]);
});
Hope this helps!

Related

Jasmine test with angular ng-mock not working with controller and promise

I have been playing a bit with the following code and I am not able to find the issue, the test fails wit the following message: "Expected undefined not to be undefined."
I have a Service which returns a promise to my controller. In the controller, I am using $q.all to do some stuff immediately after I get the promise resolved.
I tried to follow this example, but the big difference I see is that in the example it has the call in the root of the controller and I have the service call inside of the method "$scope.CustomerTest" so I have this additional line to before the apply ( $scope.$apply() ) "$scope.CustomerTest('Mr');":
http://www.bradoncode.com/blog/2015/07/13/unit-test-promises-angualrjs-q/
This is my test code:
var $scope;
var $q;
var deferred;
var $httpBackend;
//Inject the module "before each test"
beforeEach(angular.mock.module('marketingApp'));
beforeEach(inject(function ($controller,_$httpBackend_, _$rootScope_, _$q_, marketingService) {
$q = _$q_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
// We use the $q service to create a mock instance of defer
deferred = _$q_.defer();
// Use a Jasmine Spy to return the deferred promise
spyOn(marketingService, 'getTitleSuggested').and.returnValue(deferred.promise);
// Init the controller, passing our spy service instance
$controller('customerController', {
$scope: $scope,
marketingService: marketingService
});
}));
it('should resolve promise', function () {
// Setup the data we wish to return for the .then function in the controller
var titles = [{ "Id": 1, "Name": "Mr" }];
deferred.resolve(titles);
$httpBackend.when('GET', '/MarketingCustomers/GetTitleSuggested')
.respond(200, titles);
//I call to the controller method here.
$scope.CustomerTest('Mr');
$scope.$apply();
// Since we called apply, not we can perform our assertions
//expect($scope.TitlesTest).not.toBe(undefined);
expect($scope.SelectedCustomerTitle).toEqual('Mr');
//expect($scope.error).toBe(undefined);
});
And this is the plunker:
http://plnkr.co/edit/3IMzqH1yKW8kazZFWaA0?p=preview
Commenting the first test (it) of the controller.spec.js the other two test works. Any help please?
Resolved my issue here is again the plunker if someone has similar problem, hopefully it can helps.
Here is the test:
describe('CustomerController.js', function() {
var results = [{
"Id": 1,
"Name": "Mr"
}];
var $controller;
var $q;
var $rootScope;
var $scope;
var marketingService;
beforeEach(angular.mock.module('marketingApp'));
beforeEach(inject(function(_$rootScope_, $controller, _$q_,
_marketingService_) {
$scope = _$rootScope_.$new();
$q = _$q_;
$rootScope = _$rootScope_;
marketingService = _marketingService_;
$controller('customerController', {
$scope: $scope,
$rootScope: $rootScope
});
spyOn(marketingService, 'getTitleSuggested').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve(results);
return deferred.promise;
});
}));
it('It should call the service" ', function() {
$scope.CustomerTest('Mr');
expect(marketingService.getTitleSuggested).toHaveBeenCalledWith('Mr');
});
it('It should populate the "$scope.TitlesTest" ', function() {
$scope.CustomerTest('Mr');
$rootScope.$apply();
expect($scope.hello).toEqual('Hello Mr Marcos');
expect($scope.Titles[0].Name).toBe('Mr');
expect($scope.Titles).toBe(results);
});
});
and this is the plunker:
http://plnkr.co/edit/3IMzqH1yKW8kazZFWaA0?p=preview

Angular unit test factory that uses http request

I am working on an angular js app with karma/Jasmine testing framework, I need to test a factory that returns a http promise but it always return undefined
here is my factory
angular.module('GithubUsers').factory('Users',['$http','$q',function($http,$q){
return{
getAllUsers:function(){
var defered= $q.defer();
$http({
url:'https://api.github.com/users',
method:'GET'
}).then(function(users){
defered.resolve(users.data);
},function(err){
defered.reject(err);
})
return defered.promise;
}
}
}])
here is my tests
Update thanks to your answers I modified my code to the following but no I got this error
Possibly unhandled rejection: {"status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","url":"https://api.github.com/users?since=1","headers":{"Accept":"application/json, text/plain, /"},"cached":false},"statusText":""} thrown
describe('Test Users Factory',function(){
var $controller,
Users,
$rootScope,
$httpBackend,
$q;
beforeEach(module('GithubUsers'));
beforeEach(inject(function(_$controller_,_Users_,_$rootScope_,_$httpBackend_,_$q_){
$controller = _$controller_;
Users = _Users_;
$rootScope= _$rootScope_;
$httpBackend=_$httpBackend_;
}))
it('should get users',function(){
var result;
$httpBackend.whenGET('https://api.github.com/users?since=1').respond(function(){
return {data:[{id:2}],status:200};
})
Users.getAllUsers().then(function(res){
result = res;
});
$httpBackend.flush();
$rootScope.$digest()
expect(result).toBeTruthy();
})
})
Thanks in advance!
I think you need to pass a function that returns a array with 3 items in it, to whenGET().respond().
Maybe, you can try something like this:
beforeEach(angular.mock.inject(function (User, $httpBackend, $http) {
...
this.withOKUsers = function() {
var i1 = new User();
i1.id = 10;
return [200, JSON.stringify([ i1]), {}];
} ...
}));
...
it('should get users',function(){
$httpBackend
.whenGET('https://api.github.com/users')
.respond(this.withOKUsers);
Users.getAllUsers().then(function(res){
result = res;
});
$httpBackend.flush();
expect(result).not.toBeNull();
...
(I prefer to arrange spec outside of it() clause for better readability)
You're missing a $httpBackend.flush(); call after your test method call. It will invoke a success/error or then part and resolve a $q's promise properly. For more tests I would move a $httpBackend.whenGET to each test case separately so I can later verify it per use case but it's just my personal opinion.
I find it a little suspicious that you mix a $controller and a factory in one test. I would suggest to split them, and in controller test just check the calls to service methods and in a facotry test itself do a $httpBackend stuff.
Below I paste your test with my corrections. It works now for me:
describe('Test Users Factory', function () {
var Users,
$rootScope,
$httpBackend,
$q;
beforeEach(module('app.utils'));
beforeEach(inject(function (_Users_, _$rootScope_, _$httpBackend_, _$q_) {
Users = _Users_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should get users', function () {
var result;
$httpBackend.when('GET', "https://api.github.com/users").respond({ data: [{ id: 2 }], status: 200 });
Users.getAllUsers().then(function (res) {
result = res;
expect(result).toBeTruthy();
});
$httpBackend.flush();
$rootScope.$digest();
});
Important notices:
1)afterEach - check if no pending requests remain after your call
2) your url differ with a parameter ?since=1. But you do not give it as a parameter in your code so i do not understand why you added this parameter.
Maybe consider string concatenation with url and parameter ?

promise testing in angular

I want to test the following method in my controller class:
// getIds() {
// this.api.getIds()
// .then((response)=> {
// this.ids = response.data;
// this.doSomethingElse();
// });
// }
I'm not sure how to handle the promise using jasmine and karma. The project is written in ES6. api.getIds() returns a $http.get().
beforeEach(function() {
inject(function($controller, $rootScope, _api_) {
vm = $controller('MainController', {
api: _api_,
$scope:$rootScope.$new()
});
});
});
beforeEach(function () {
vm.getIds();
});
it('should set the ids', function () {
expect(vm.ids).toBeDefined(); //error
});
How do I wait for the promise to complete before running the expect() ?
First of all, you should use the done callback provided by the jasmine; see async support in Jasmine.
Then, you should mock your getIds on the api so that it returns a resolved promise with an expected value. The asserts should be done after the then promise is called - se bellow the full example.
beforeEach(function () {
var $q, vm, api, $controller, $rootScope;
inject(function (_$controller_, _$rootScope_, _$q_) {
$q = _$q_;
$controller = _$controller_;
$rootScope = _$rootScope_;
api = jasmine.createSpyObj('api', ['getIds']);
api.getIds.and.returnValue($q.when([]));
vm = $controller('MainController', {
api: api,
$scope: $rootScope.$new()
});
});
});
it('should set the ids', function (done) {
vm
.getIds()
.then(function (ids) {
expect(ids).toBeDefined();
// add more asserts
done();
});
});
As a side note, if the this.doSomethingElse(); is a promise too, you have to return it in the first then so that you can test the final result.

Jasmine: Test void function that contains async call

I'd like to know which would be the best way to test functions that returns nothing(just changes a field value) and contains an async call.
This the AngularJS controller I want to test, the service I call returns a promise(always returns {name:"John"}):
app.controller('MyCtrl', function($scope, AsyncService) {
$scope.greeting = "";
$scope.error =
$scope.sayHello = function() {
AsyncService.getName().then(
function(data){
$scope.saludo = "Hello " + data.name;
},
function(data){
$scope.error = data;
}
);
};
});
This would be the spec if the sayHello function did not contain an async call, but it always fails because scope.greeting is always empty.
describe('Test My Controller', function() {
var scope, $httpBackend;
// Load the module with MainController
//mock Application to allow us to inject our own dependencies
beforeEach(angular.mock.module('app'));
//mock the controller for the same reason and include $rootScope and $controller
beforeEach(angular.mock.inject(function($rootScope, $controller,_$httpBackend_){
//Mock the service to always return "John"
$httpBackend = _$httpBackend_;
$httpBackend.when('POST', 'http://localhost:8080/greeting').respond({name: "John"});
//create an empty scope
scope = $rootScope.$new();
//declare the controller and inject our empty scope
$controller('MyCtrl', {$scope: scope});
}));
it('$scope.greeting should get filled after sayHello', function() {
expect(scope.greeting).toEqual("");
scope.sayHello();
expect(scope.greeting).toEqual("Hello John");
});*/
});
How would I make this spec to handle the async call? I don't really understand how and where to use the "done" flag of Jasmine 2.0.
Use $q.defer() to return a promise from the getName function in a mock of your service. Then pass the mocked into the dependancies when your controller is created:
beforeEach(inject(function($controller, _$rootScope_, $q) {
$rootScope = _$rootScope_;
deferred = $q.defer();
asyncService = {
getName: function () {
}
};
spyOn(asyncService, 'getName').and.returnValue(deferred.promise);
$scope = $rootScope.$new();
createController = function() {
return $controller('MyCtrl', { $scope: $scope, AsyncService: asyncService } );
};
}));
Then after you call $scope.hello() call deferred.resolve(data)l where data is the data that you want returned from your service in the promise. Then call $rootScope.$digest();
it('$scope.saludo should get filled after sayHello', function() {
//Arrange
var controller = createController();
var data = {
name: 'John'
};
//Act
$scope.sayHello();
deferred.resolve(data);
$rootScope.$digest();
//Assert
expect($scope.saludo).toEqual('Hello ' + data.name);
});
Plunkr

How to test saving a resource in a controller with a promise

I have a controller that saves a resource. I can't tell how to "access" the part of the code that executes after the promise resolves. What do I need to change about my test or controller in order to get it to work? Here's the code.
Controller:
'use strict';
/**
* #ngdoc function
* #name lunchHubApp.controller:AnnouncementsCtrl
* #description
* # AnnouncementsCtrl
* Controller of the lunchHubApp
*/
angular.module('lunchHubApp')
.controller('AnnouncementsCtrl', ['$scope', 'Announcement', function ($scope, Announcement) {
$scope.announcements = [];
$scope.save = function() {
// This next line is the part I'm finding hard to test.
new Announcement($scope.announcement).create().then(function(announcement) {
$scope.foo = 'bar'
});
};
}]);
Test:
'use strict';
describe('AnnouncementsCtrl', function() {
beforeEach(function() {
module('lunchHubApp', 'ng-token-auth')
});
it('sets scope.announcements to an empty array', inject(function($controller, $rootScope) {
var scope = $rootScope.$new(),
ctrl = $controller('AnnouncementsCtrl', { $scope: scope });
expect(scope.announcements).toEqual([]);
}));
describe('save', function() {
it('works', inject(function($controller, $rootScope, _$httpBackend_) {
var $httpBackend = _$httpBackend_;
var scope = $rootScope.$new(),
ctrl = $controller('AnnouncementsCtrl', { $scope: scope });
expect(scope.announcements.length).toBe(0);
var announcement = {
restaurantName: 'Bangkok Taste',
userId: 1
};
scope.announcement = announcement;
$httpBackend.expect('POST', '/api/announcements').respond(200, announcement);
scope.save();
scope.$digest();
expect(scope.foo).toEqual('bar');
}));
});
});
Update: here's the way I ended up modifying my controller test. The following passes and has been refactored from the original.
'use strict';
describe('AnnouncementsCtrl', function() {
var $httpBackend,
announcement,
scope,
ctrl;
beforeEach(function() {
module('lunchHubApp');
inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
scope = $injector.get('$rootScope').$new();
ctrl = $injector.get('$controller')('AnnouncementsCtrl', { $scope: scope });
announcement = { restaurantName: 'Bangkok Taste' };
scope.announcement = { restaurantName: 'Jason\'s Pizza' };
$httpBackend.expect('GET', '/api/announcements').respond([announcement]);
});
});
it('sets scope.announcements to an empty array', function() {
expect(scope.announcements).toEqual([]);
});
it('grabs a list of announcements', function() {
expect(scope.announcements.length).toBe(0);
$httpBackend.flush();
expect(scope.announcements.length).toBe(1);
});
describe('save', function() {
beforeEach(function() {
$httpBackend.expect('POST', '/api/announcements').respond(200, { restaurantName: 'Foo' });
scope.save();
$httpBackend.flush();
});
it('adds an announcement', function() {
expect(scope.announcements.length).toBe(2);
});
it('clears the restaurant name', function() {
expect(scope.announcement.restaurantName).toEqual('');
});
});
});
I think what you're doing is good. Since the Angular resources are factories using the $http service in a restful way, you should use the expect of the $httpBackend just as you did.
One thing that you miss however is that you need to make sure your promise is resolved. But write async tests can be tricky in some cases. To do so, you have to use the flush() method of $httpBackend to force your test to be synchronous.
After the flush, you can make your expect normally. Also you might have to move your expectPOST before your $rootScope.$new() statement.
You can go with a change like this, I don't think the $digest() is necessary:
$httpBackend.expect('POST', '/api/announcements').respond(200, announcement);
scope.save();
$httpBackend.flush();
expect(scope.foo).toEqual('bar');
The tests you've started writing seem to be testing not just AnnouncementsCtrl, but the Announcements service/factory as well. The signs of this in this case are
You're not mocking the Announcements service/factory / not stubbing any of its methods.
There is no code in the AnnouncementsCtrl regarding making http requests, and yet you're using $httpBackend.expect(... in the tests for them.
The success/failure of the tests that claim to test AnnouncementsCtrl will succeed or fail depending on code in the Announcements service/factory.
This goes against what unit tests are usually used for: testing each component in isolation. Keeping the focus of this answer on testing the success callback passed to the then method of the promise returned by create, my suggestion is to mock the Announcements service/factory, so its create method returns a promise that you can control in the test. This mock would be of the form:
var MockAnnouncement = null;
var deferred = null;
beforeEach(module(function($provide) {
MockAnnouncement = function MockAnnouncement() {
this.create = function() {
return deferred.promise;
};
};
$provide.value('Announcement', MockAnnouncement);
}));
You would then have to make sure that you create deferred object before each test:
beforeEach(inject(function($rootScope, $controller, $q) {
$scope = $rootScope.$new();
deferred = $q.defer(); // Used in MockAnnouncement
ctrl = $controller('AnnouncementsCtrl', {
$scope: $scope
});
}));
This deferred object is then resolved in the test:
it('calls create and on success sets $scope.foo="bar"', function() {
$scope.save();
deferred.resolve();
$scope.$apply();
expect($scope.foo).toBe('bar');
});
A slightly extended version of this, testing a few other behaviours of the controller as well, can be seen at http://plnkr.co/edit/v1bCfmSPmmjBoq3pfDsk

Resources