Ebrahim Ze Ebrahim Ze - 2 months ago 19
AngularJS Question

How to handle promise on success or on error

I wrote an ionic app. I started adding tests to it I'm facing a problem with

$resources
. In this case I have this Controller :



.controller('newAccountCtrl', function($scope, $window, $rootScope, API, $ionicPopup, $state) {
$scope.newData = {};
$scope.$on('$ionicView.enter', function() {

$scope.newData = {};
});
$scope.newInfo = function() {
API.newAccountInfo.update({
restCode: $scope.newData.restore_code
}, $scope.newData, function(res, header) {
$scope.comty = 'update';
$window.location.href = '#/login';
}, function(err) {
if (err.data == null)
$rootScope.popup("Error", "no connection");
else
$rootScope.popup('error', err.data.error);
});
}
})





And in the service I make a request using
$resources
in function :



angular.module('starter.services', [])
.factory('API', function($rootScope, $resource, $ionicPopup, $ionicLoading, $window) {
return {
newAccountInfo: $resource(base + '/restoreinfo/:restCode', {
restCode: '@_restCode'
}, {
update: {
method: 'PUT'
}
}, {
stripTrailingSlashes: false
});
}





and in the my test the following code:



describe('newAccountCtrl', function() {

var controller,
deferredLogup, window, scope,
$rootScope,
$q, store, API,
PROMISE = {
resolve: true,
reject: false
};
beforeEach(angular.mock.module('starter'));
beforeEach(module('starter.controllers'));
beforeEach(module('starter.services'));
beforeEach(module(function($provide, $urlRouterProvider) {
$provide.value('$ionicTemplateCache', function() {});
$urlRouterProvider.deferIntercept();
}));

beforeEach(inject(function($controller, _$rootScope_, $q, _API_, _$window_) {
$q = $q;

deferredLogup = $q.defer();
$rootScope = _$rootScope_;
spyOn($rootScope, 'popup')
scope = $rootScope.$new();
API = _API_;
window = _$window_;

spyOn(API.newAccountInfo, 'update').and.callThrough(function(callback) {
deferredLogup.promise.then(callback);
return deferredLogup.promise;
});

controller = $controller('newAccountCtrl', {
'$scope': scope,
'API': API,
'$window': window
});

}));
it('expect API.newAccountInfo to Have Been Called', function() {
scope.getloghist();
expect(API.newAccountInfo.upadate).toHaveBeenCalled();
})

it('on success ', function() {
scope.newInfo();
deferredLogup.resolve();
scope.$digest();
expect(scope.comty).toBeDefined();
});

it('on unsuccessful', function() {
scope.newInfo();
deferredLogup.reject();
scope.$digest()
expect($rootScope.popup).toHaveBeenCalled();
});
});





first expect it pass , but the second one "on success" return with error


"Expected undefined to be defined".


I'm new to writing unit tests. What am I missing here?

Answer

In your spy:

spyOn(API.newAccountInfo, 'update').and.callThrough(function(callback){
  deferredLogup.promise.then(callback);
  return deferredLogup.promise;
});

.and.callThrough only tracks the function. It does not mock it (thus the function you pass it is doing nothing). From the docs:

By chaining the spy with and.callThrough, the spy will still track all calls to it but in addition it will delegate to the actual implementation.

Which is fancy talk for 'it runs the actual function that you're spying on'.

Looks like you want .and.callFake which tracks and mocks with the passed function.

Edit because it looks like there's more than one thing wrong:

spyOn(API.newAccountInfo,'update').and.callFake(function(callback){
  deferredLogup.promise.then(callback);
  return deferredLogup.promise;
});

So, you're mocking with this anonymous function:

function(callback){
  deferredLogup.promise.then(callback);
  return deferredLogup.promise;
}

This^ function is now acting as API.newAccountInfo.update. This function takes its first parameter and runs it as a callback. Go look at where you're running API.newAccountInfo.update:

API.newAccountInfo.update({
  restCode: $scope.newData.restore_code
}, $scope.newData, function(res, header) {
  $scope.comty = 'update';
  $window.location.href = '#/login';
}, function(err) {
  if (err.data == null)
    $rootScope.popup("Error", "no connection");
  else
    $rootScope.popup('error', err.data.error);
});

//Simplifying this:

API.newAccountInfo.update([Object], [Object], successCallback, errorCallback)

You're passing the callback as the third and fourth parameters, not the first one. But your mock is trying to run the first parameter. So it's trying to feed an object (not a function) into deferredLogup.promise.then.

Your spy should look like this:

spyOn(API.newAccountInfo,'update').and.callFake(function(param1, param2, callback, errorCallback){
  deferredLogup.promise.then(callback);
  return deferredLogup.promise;
});

Hopefully that helps.