I have some code that runs a timer when user is active and broadcasts and event to keep the session alive.
In the controller I have some code to listen for that event and refresh the session.
I want to test this listener
$scope.$on('Keepalive', function () {
//every 45 minutes make a call to refresh the session.
var promise = authService.keepAliveSession();
promise.then(function(userPreferenceData) {
dataTransfer.setUserPref(userPreferenceData);
}, function(error) {
console.log("promise error!!"+error);
});
});
my keepalive service returns a promise which will get resolved after the httpbackend returns.
factory.keepAliveSession = function () {
var deferred = $q.defer();
req=...some stuff...
$http(req)
.success(
function (data, status) {
if ( data.user ) {
// Received data about the logged in user
deferred.resolve(factory.userPreferenceData);
} else {
// User is not authenticated; redirect to login
$window.location = data.redirect;
}
}
).error(function (error) {
// Error in oAuth refresh service
deferred.reject("Error in session keepalive"+ error);
});
return deferred.promise;
}
here is the test
it('Test for keepalive', function() {
console.log('starting for keepalive...');
httpBackend.when('POST', "http://example.com/refreshSession").respond(getMocks().response.oAuthResponseExternal);
spyOn(authServiceMock, "keepAliveSession").and.callThrough();
spyOn(dataTransferMock, "setUserPref").and.callThrough();
rootScope.$broadcast('Keepalive');
expect(authServiceMock.keepAliveSession).toHaveBeenCalled();
rootScope.$digest;
expect(dataTransferMock.setUserPref).toHaveBeenCalled();
});
The first assertion is successful, but the second one (after the digest) fails.
How can I force the promise to be resolved or rejected?
Do I need to somehow mock the $q?
It's hard to say for sure without seeing the mock implementations, how you wire up the module and how you create the controller.
My guess is that the promise that is returned from authServiceMock.keepAliveSession never gets resolved, which would lead to that the success function that dataTransfer.setUserPref lives in never gets executed.
If you for example have a mock that looks like this:
var deferred;
var authServiceMock = {
keepAliveSession: function () {
deferred = q.defer();
return deferred.promise;
}
};
You need to manually in your test either resolve or reject the promise before you trigger the digest cycle, depending on which case you are testing:
expect(authServiceMock.keepAliveSession).toHaveBeenCalled();
deferred.resolve('something');
$rootScope.$digest();
expect(dataTransferMock.setUserPref).toHaveBeenCalled();
Note that you need to execute the $digest function, in your example you just have rootScope.$digest;.
On another note, it seems to me you are mixing testing concerns a bit.
From my point of view, this is what your controller should test:
When the Keepalive event is fired - authService.keepAliveSession should be called
If the promise from authService.keepAliveSession is resolved - dataTransfer.setUserPref should be called and pass the correct data
If the promise from authService.keepAliveSession is rejected - an error message should be logged
The implementation details of the services shouldn't matter (other than that authService.keepAliveSession returns a promise) and you shouldn't need to involve httpBackend in this case.
Setting up fake return data with httpBackend should be used when you test the actual service that uses the $http service.
Below is an alternative way to test this, using spyOn and callFake instead of using mock implementations.
describe('myApp', function() {
var $rootScope;
var $controller;
var authService;
var dataTransfer;
var $log;
var myController;
beforeEach(function() {
module('myApp');
inject(function(_$rootScope_, _$controller_, _authService_, _dataTransfer_, _$q_, _$log_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
authService = _authService_;
dataTransfer = _dataTransfer_;
$q = _$q_;
$log = _$log_;
});
myController = $controller('MyController', {
$scope: $rootScope.$new()
});
});
it('On event "Keepalive" - should call "authService.keepAliveSession"', function() {
spyOn(authService, 'keepAliveSession').and.callFake(function() {
var deferred = $q.defer();
return deferred.promise;
});
$rootScope.$broadcast('Keepalive');
expect(authService.keepAliveSession).toHaveBeenCalled();
});
it('Promise from "authService.keepAliveSession" is resolved - should call "dataTransfer.setUserPref"', function() {
var data = {};
spyOn(authService, 'keepAliveSession').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve(data);
return deferred.promise;
});
spyOn(dataTransfer, 'setUserPref');
$rootScope.$broadcast('Keepalive');
$rootScope.$digest();
expect(dataTransfer.setUserPref).toHaveBeenCalledWith(data);
});
it('Promise from "authService.keepAliveSession" is rejected - should not call "dataTransfer.setUserPref"', function() {
var data = {};
spyOn(authService, 'keepAliveSession').and.callFake(function() {
var deferred = $q.defer();
deferred.reject(data);
return deferred.promise;
});
spyOn(dataTransfer, 'setUserPref');
$rootScope.$broadcast('Keepalive');
$rootScope.$digest();
expect(dataTransfer.setUserPref).not.toHaveBeenCalled();
});
it('Promise from "authService.keepAliveSession" is rejected - should log message', function() {
var error = {};
spyOn(authService, 'keepAliveSession').and.callFake(function() {
var deferred = $q.defer();
deferred.reject(error);
return deferred.promise;
});
spyOn($log, 'log');
$rootScope.$broadcast('Keepalive');
$rootScope.$digest();
expect($log.log).toHaveBeenCalledWith(error);
});
});
Demo: http://plnkr.co/edit/L1uks0skH2N5bAXGoe90?p=preview
Related
I have to unit test my controller. First I have to create mock for my services.
Here is my service:
angular.module("demo-app")
.factory("empService",function($http){
var empService={};
empService.getAllEmployees=function(){
return $http.get("http://localhost:3000/api/employees");
}
empService.postEmployee=function(emp){
return $http.post("http://localhost:3000/api/employees",emp);
}
empService.getEmployee=function(id){
return $http.get("http://localhost:3000/api/employees/"+id)
}
empService.putEmployee=function(emp){
return $http.put("http://localhost:3000/api/employees/"+emp._id,emp)
}
empService.deleteEmployee=function(id){
return $http.delete("http://localhost:3000/api/employees/"+id);
}
empService.findEmployee=function(emp){
return $http.post("http://localhost:3000/api/employees/search",emp);
}
return empService;
})
Here is findData() method in my controller, which I am going to test:
$scope.findData=function(){
$scope.loadingEmployee=true;
var emp={};
listProp=Object.getOwnPropertyNames($scope.searchEmployee);
for(index in listProp){
if($scope.searchEmployee[listProp[index]]!=""){
emp[listProp[index]]=$scope.searchEmployee[listProp[index]];
}
}
console.log(emp);
empService.findEmployee(emp).then(function(data){
$scope.allEmployees=data.data;
console.log(data.data);
$scope.loadingEmployee=false;
});
}
How can I mock my empService.findEmployee(emp) method, so that I can test the findData() method.
My spec.js test file with mocking my service method. Here it is:
beforeEach(function(){
var emp={"name":"sanjit"};
fakeService={
getAllEmployees:function(emp){
def=q.defer();
def.resolve({data:[{"name":"sanjit"},{'name':'ssss'}]});
return def.promise;
},
findEmployee:function(emp){
var def=q.defer();
def.resolve({data:[{"name":"sanjit"}]});
console.log("working");
return def.promise;
}
};
spyOn(fakeService,'findEmployee').and.callThrough();
fakeService.findEmployee(emp);
});
beforeEach(angular.mock.inject(function($rootScope,$controller,$injector,$q){
httpBackend=$injector.get('$httpBackend');
scope=$rootScope.$new();
q=$q;
ctrl=$controller('adminEmployeeCtrl',{$scope:scope,empService:fakeService});
}));
it('findData test',function(){
scope.$apply();
scope.findData();
expect(scope.loadingEmployee).toEqual(false);
})
But I got another error:
Error: Unexpected request: GET dashboard/views/dashboard-new.html
No more request expected
But I didn't call it. Please help me
You may not have manually called GET dashboard/views/dashboard-new.html but $scope.$apply() might be triggering it somehow and you can't do anything but handle it.
You can do something like this to handle it: (after injecting it using _$httpBackend_ and assigning to $httpBackend in beforeEach)
$httpBackend.when('GET', 'dashboard/views/dashboard-new.html').respond(200);
scope.$digest();
$httpBackend.flush();
One of the most important rules when testing controllers in angularjs is you do not need to create reall http requests, just mock the functions in that service that are used by your controller. So you need to spyOn them and call fake function to return the proper value. Let's spy on one of them
/**
* #description Tests for adminEmployeeCtrl controller
*/
(function () {
"use strict";
describe('Controller: adminEmployeeCtrl ', function () {
/* jshint -W109 */
var $q, $scope, $controller;
var empService;
var errorResponse = 'Not found';
var employeesResponse = [
{id:1,name:'mohammed' },
{id:2,name:'ramadan' }
];
beforeEach(module(
'loadRequiredModules'
));
beforeEach(inject(function (_$q_,
_$controller_,
_$rootScope_,
_empService_) {
$q = _$q_;
$controller = _$controller_;
$scope = _$rootScope_.$new();
empService = _empService_;
}));
function successSpies(){
spyOn(empService, 'findEmployee').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve(employeesResponse);
return deferred.promise;
// shortcut can be one line
// return $q.resolve(employeesResponse);
});
}
function rejectedSpies(){
spyOn(empService, 'findEmployee').and.callFake(function () {
var deferred = $q.defer();
deferred.reject(errorResponse);
return deferred.promise;
// shortcut can be one line
// return $q.reject(errorResponse);
});
}
function initController(){
$controller('adminEmployeeCtrl', {
$scope: $scope,
empService: empService
});
}
describe('Success controller initialization', function(){
beforeEach(function(){
successSpies();
initController();
});
it('should findData by calling findEmployee',function(){
$scope.findData();
// calling $apply to resolve deferred promises we made in the spies
$scope.$apply();
expect($scope.loadingEmployee).toEqual(false);
expect($scope.allEmployees).toEqual(employeesResponse);
});
});
describe('handle controller initialization errors', function(){
beforeEach(function(){
rejectedSpies();
initController();
});
it('should handle error when calling findEmployee', function(){
$scope.findData();
$scope.$apply();
// your error expectations
});
});
});
}());
I know that this error appears in questions a lot but despite my efforts I didn't manage to solve my problem.
I have a Service that fetches some categories from Parse. This is done successfully when running my test but I am getting the error shown in the title.
My code is the following:
ParseService.js
angular.module('starter').service('ParseService', ['$q', function ($q) {
this.getAllCategories = function() {
var deferred = $q.defer();
var categories = [];
var query = new Parse.Query("Category");
query.find({
success: function(results) {
console.log('Succesfully queried Parse.');
for (var i = 0; i < results.length; i++) {
var result = results[i];
categories.push(result.get("name"));
}
deferred.resolve(categories);
console.log(categories);
},
error: function(error) {
console.log('Error querying Parse.');
console.log(error);
deferred.reject(error);
}
});
return deferred.promise;
};
}]);
ParseServiceTest.js
describe('ParseService', function(){
var service;
beforeEach(function(){
module('starter');
});
it ('should fetch Categories', function(done){
beforeEach(inject(function(ParseService){
service=ParseService;
}));
var promise = service.getAllCategories();
promise.then(function(categories){
expect(categories.length).toEqual(5);
done();
});
});
});
Now the console output is the following:
'Succesfully queried Parse.'
['Pets', 'Automobile', 'Social', 'Retail', 'Technology']
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
It seems like the service returns the categories successfully but the done() method is never executed for some reason.
You can ignore the fact that I don't mock the call to Parse. I am aware that this is not the best practice.
For resolving/rejecting $q promises in tests, you need to start the digest cycle.
describe('ParseService', function() {
var service;
var $rootScope;
beforeEach(module('starter'));
beforeEach(inject(function(ParseService, _$rootScope_) {
service = ParseService;
$rootScope = _$rootScope_;
}));
it('should fetch Categories', function(done) {
var promise = service.getAllCategories();
promise.then(function(categories) {
expect(categories.length).toEqual(5);
done();
});
// run digest cycle to resolve $q promises
$rootScope.$digest();
});
});
I am calling an API service which returns a promise from a factory.
Here is a part of my factory.
factories.factory('OnBoardingFactory', ['$http',
function ($http) {
var dataFactory = {};
dataFactory.get = function (url) {
return $http.get('http://localhost/api/onboarding/' + url)
};
return dataFactory
}
]);
And here is where its called from the controller:
OnBoardingFactory.get('login?username=test&password=password')
.then(function(response){
$scope.response = response.status;
})
This returns data in the controller absolutely fine. However I have difficulties when I come to test it. Here is my test script:
var scope, FakeOnBoardingFactory, controller, q, deferred;
beforeEach(module('app.module'));
beforeEach(function () {
FakeOnBoardingFactory = {
get: function () {
deferred = q.defer();
// Place the fake return object here
deferred.resolve({ response: {status: 200}});
return deferred.promise;
}
};
spyOn(FakeOnBoardingFactory, 'get').and.callThrough();
});
beforeEach(inject(function ($q, $rootScope, $controller, $injector ) {
scope = $rootScope.$new();
q = $q;
controller = $controller(OnBoardingCtrl, {
$scope: scope,
OnBoardingFactory: FakeOnBoardingFactory
})
}));
it('Should call the form and return 200', function () {
// Execute form
scope.loginCredentials({$valid: true});
scope.$apply();
// Ensure script is called (which passes fine)
expect(FakeOnBoardingFactory.get).toHaveBeenCalled();
scope.$apply();
// BREAKS HERE
expect(scope.status).toBe(200);
})
When expect(FakeOnBoardingFactory.get).toHaveBeenCalled(); is called, this passes fine. However then I run expect(scope.status).toBe(200), it breaks "Expected undefined to be 200".
This would indicate that my FakeOnBoardingFactory isn't returning any data. But I can't seem to find the issue.
It must be the change to support multiple body assertions that has caused this bug.
The workaround for now is to either don't use expect and do your assertion in the end function callback.
So instead of .expect(200) it would be.
end(function(err,res) { res.status.should.equal(200) },
or if you do use expect.. you need to make sure you specify a body as well as just a status..
it('should assert status only 1', function(done){
var app = express();
app.get('/user', function(req, res){
res.send(201, { name: 'tobi' }); }); request(app) .get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '20')
.expect(201)
.end(function(err, res){
if (err) throw err;
});
})
I have the following function in my controller:
$scope.submitNote = function(){
myService.addNote($scope.note).then(function(data){
if(data.success === true){
$scope.note = null;
}
else{
// API call failed
}
}, function(){
// Promise call failed
});
};
I set up my testing environment with:
// Mock out fake service
beforeEach(function(){
myService = {
addNote: function(){
deferred = q.defer();
deferred.resolve({
success: true
});
return deferred.promise;
}
};
spyOn(myService, 'addNote').and.callThrough();
});
// Assign controller scope
beforeEach(inject(function($controller, $rootScope, $q){
q = $q;
scope = $rootScope.$new();
$controller('myController', {
$scope: scope,
myService: myService
});
}));
Then test out my submitNote() function with:
describe('submitNote Test', function(){
it('should set scope.note to null after successful service call', function(){
scope.submitNote();
expect(myService.addNote).toHaveBeenCalled();
expect(scope.note).toBe(null);
});
});
The first expect passes, but the second expect does not. It looks like the then() callback from my submitNote() function isn't being called in the test.
How do I make sure the promise callback in the original function is called?
To give you cleaner tests that you have more control over the ngMock module extends various core services so they can be inspected and controlled in a synchronous manner.
Promise callbacks are executed during the digest loop, which in your testing environment you need to start manually.
For example:
describe('submitNote Test', function () {
it('should set scope.note to null after successful service call', function () {
scope.submitNote();
scope.$digest();
expect(myService.addNote).toHaveBeenCalled();
expect(scope.note).toBe(null);
});
});
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!