I'm looking to test nested promises and I'm not sure how to go about this.
How would I go about testing a scenario like this?
angular.module('moduleA')
.factory('serviceA', function(serviceB){
return {
create: create
}
function create(){
return serviceB.create().then(function(d){
serviceB.doSomething();
return $state.go("app.list", { id: d.id }).then(function(){
serviceB.doSomethingElse();
});
})
};
});
Where I would like to test
expect(serviceB, 'create').toHaveBeenCalled();
expect($state, 'go').toHaveBeenCalledWith("app.list", { id: newId });
expect(serviceB, 'doSomething').toHaveBeenCalled();
expect(serviceB, 'doSomethingElse').toHaveBeenCalled();
Here is my attempt:
describe('serviceA spec', function () {
var $rootScope, $state, serviceA, serviceB;
beforeEach(function () {
$state = {
go: function(){}
}
serviceB = {
create: {},
doSomething: {},
doSomethingElse: {}
}
module('app', function($provide){
$provide.value('serviceB', serviceB);
$provide.value('$state', $state);
});
inject(function (_$rootScope_, _serviceA_) {
serviceA =_serviceA_;
$rootScope = _$rootScope_;
});
});
it('should create', function(){
var newId = "555";
spyOn($state, 'go').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve({});
return deferred.promise;
});
spyOn(serviceB, 'create').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve({ id: newId });
return deferred.promise;
});
spyOn(serviceB, 'doSomething');
spyOn(serviceB, 'doSomethingElse');
serviceA.create();
expect(serviceB, 'create').toHaveBeenCalled();
expect($state, 'go').toHaveBeenCalledWith("app.list", { id: newId });
expect(serviceB, 'doSomething').toHaveBeenCalled();
expect(serviceB, 'doSomethingElse').toHaveBeenCalled();
$rootScope.$digest();
});
});
Any suggestions would be much appreciated
After each promise you have to call $rootScope.$apply() so promises are fullfilled.
Related
Im new to angular and I'm having trouble saving user info from LinkedIn API to the scope in my controller without passing the scope to my custom service. I assume that is not the angular way of programming.
//html
<script type="text/javascript" src="//platform.linkedin.com/in.js">
api_key: *********
onLoad: onLinkedInLoad
</script>
// linkedIn button
<script type="in/Login">
</script>
// app.js
angular.module("linkedinTestApp",[]);
function onLinkedInLoad(){
eScope.$apply(function(){
eScope.getLinkedInData();
})
};
// main controller
var eScope;
angular.module("linkedinTestApp").
controller('mainCtrl',function($scope,linkedInService){
eScope = $scope;
$scope.getLinkedInData = function(){
linkedInService.OnLinkedInFrameworkLoad($scope);
}
})
//custom service
angular.module('linkedinTestApp')
.service('linkedInService', function() {
var scope;
this.OnLinkedInFrameworkLoad = function(s) {
scope = s;
IN.Event.on(IN, "auth", this.OnLinkedInAuth);
console.log("Test1");
}
this.OnLinkedInAuth = function() {
IN.API.Profile("me").result(function(result){
console.log(result);
var profile = {
vnaam: result.values[0].firstName,
anaam: result.values[0].lastName,
foto: result.values[0].pictureUrl,
headline: result.values[0].headline,
id: result.values[0].id
}
console.log(profile);
scope.profile = profile;
});
console.log("Test2");
}
});
Tested code. Took me 20-30 minutes to get api key and when i tested someone posted answer, but my code was tested so a post this, similar, answer. Also this is not the most elegant way to get profile in the controller, but I wanted to change as little code as possible(for similary).
angular.module("linkedinTestApp",[]);
function onLinkedInLoad(){
eScope.$apply(function(){
eScope.getLinkedInData();
})
};
// main controller
var eScope;
angular.module("linkedinTestApp").
controller('mainCtrl',function($scope,linkedInService){
eScope = $scope;
$scope.getLinkedInData = function(){
linkedInService.OnLinkedInFrameworkLoad().then(function(profile){
console.log('response ', profile);
});
}
})
//custom service
angular.module('linkedinTestApp')
.service('linkedInService', function($q) {
this.OnLinkedInFrameworkLoad = function() {
var deferred = $q.defer();
IN.Event.on(IN, "auth", function(){
deferred.resolve(OnLinkedInAuth())
});
return deferred.promise;
}
function OnLinkedInAuth() {
var deferred = $q.defer();
IN.API.Profile("me").result(function(result){
console.log(result);
var profile = {
vnaam: result.values[0].firstName,
anaam: result.values[0].lastName,
foto: result.values[0].pictureUrl,
headline: result.values[0].headline,
id: result.values[0].id
}
deferred.resolve(profile);
});
return deferred.promise;
}
});
// main controller
angular.module("linkedinTestApp").
controller('mainCtrl',function($scope,linkedInService){
$scope.getLinkedInData = function(){
linkedInService.OnLinkedInFrameworkLoad().then (function (result) {
$scope.profile = result;
});
}
})
//custom service
angular.module('linkedinTestApp')
.service('linkedInService', function() {
this.OnLinkedInFrameworkLoad = function() {
return this.OnLinkedInAuth();
}
this.OnLinkedInAuth = function() {
return IN.API.Profile("me").result(function(result){
console.log(result);
var profile = {
vnaam: result.values[0].firstName,
anaam: result.values[0].lastName,
foto: result.values[0].pictureUrl,
headline: result.values[0].headline,
id: result.values[0].id
}
console.log(profile);
return profile;
});
}
});
My suggestion:
angular.module('linkedinTestApp').service('linkedInService', function($q) {
var deferred = $q.defer();
var self = this;
this.profile = null;
this.OnLinkedInFrameworkLoad = function() {
IN.Event.on(IN, "auth", this.OnLinkedInAuth);
console.log("Test1");
} // NOT SURE IF THIS FUNCTION IS NEEDED, OR WHO CALLS IT, MAYBE YOU NEED TO JUST REPLACE IT WITH THE NEXT LINE:
// IN.Event.on(IN, "auth", this.OnLinkedInAuth);
this.OnLinkedInAuth = function() {
IN.API.Profile("me").result(function(result){
console.log(result);
deferred.resolve( {
vnaam: result.values[0].firstName,
anaam: result.values[0].lastName,
foto: result.values[0].pictureUrl,
headline: result.values[0].headline,
id: result.values[0].id
} );
});
console.log("Test2");
}
this.instance = function() {
return deferred.promise;
}
});
And use it in your controller:
$scope.linkedInService.instance().then(
function(profile) {
console.log(profile);
}
);
Of course I haven't tested it, but I hope it will work...
I recently started to learn unit test for angular apps. And already faced up with problem. I can not take scope variable from inside executed function. Here is my factory code
angular.module('app').factory('AuthenticationService', AuthenticationService);
AuthenticationService.$inject = ['$http'];
function AuthenticationService($http) {
var service = {};
service.login = login;
return service;
function login(data, callback) {
$http({
method: 'POST',
url: CONFIG.getUrl('auth/login'),
data: data
}).then(function (response) {
callback(response);
}, function (error) {
callback(error);
});
}
Part of my controller file. I only yet wan to test login function
function AuthCtrl($scope, $location, AuthenticationService) {
var vm = this;
vm.login = login;
vm.dataLogin = {
user_id: '',
password: '',
};
function login() {
vm.dataLoading = true;
AuthenticationService.login(vm.dataLogin, function (response) {
if (response.status == 200) {
if (response.data.error_code == 'auth.credentials.invalid') {
vm.invalidCredentials = true;
} else {
vm.invalidCredentials = false;
if (response.data.session_state == 'otp_required') {
vm.userNumber = response.data.user_phone;
$localStorage['session_token'] = response.data.session_token;
vm.needForm = 'someForm';
} else {
AuthenticationService.setCredentials(response.data);
$state.go('dashboard');
}
vm.dataLoading = false;
}
}
});
}
}
});
And my spec.js
describe('AuthCtrl, ', function() {
var $scope, ctrl;
var authSrvMock;
var mockJson = {
user_id: '001',
session_token: 'some_token'
};
var mockLoginData = {
user_id: '0000102',
password: '123456'
};
var mockResponseData = {
data: {
"session_expires": 1453822506,
"session_state": "otp_required",
"session_token": "tokennnn",
"status": "success",
"user_id": "0000102",
"user_phone": "+7 (XXX) XXX-XX-89"
},
status: 200
};
beforeEach(function () {
authSrvMock = jasmine.createSpyObj('AuthenticationService', ['login', 'logout']);
module('app');
inject(function ($rootScope, $controller, $q) {
$scope = $rootScope.$new();
authSrvMock.login.and.returnValue(mockResponseData);
ctrl = $controller('AuthCtrl', {
$scope: $scope,
AuthenticationService: authSrvMock
});
});
});
it('should call login function and pass to dashboard', function () {
ctrl.login();
expect(authSrvMock.login).toHaveBeenCalled();
// until this everything works here just fine
});
});
But after I want to test vm.invalidCredentials, if I will write
expect(ctrl.invalidCredentials).toBe(false)
I will get the error
Expected undefined to be false.
Why I can't see variables?
Bit of a noob myself at Jasmine, but I'm guessing it's because you need to get the promise from your login() to return in Jasmine.
Look into using $q.defer(), or even $httpBackend.
After some more digging process and experiments I found solution.
Here what I did
(function () {
'use strict';
describe('AuthCtrl', function () {
var controller, scope, myService, q, deferred, ctrl;
var mockResponseData = {
response1: {
//...
},
response2: {
//...
},
response3: {
//...
}
};
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope, $q, $httpBackend, AuthenticationService) {
function mockHttp(data, callback) {
deferred = $q.defer();
deferred.promise.then(function (response) {
callback(response);
}, function (error) {
callback(error);
});
}
controller = $controller;
scope = $rootScope.$new();
myService = AuthenticationService;
q = $q;
myService.login = mockHttp;
}));
describe('when returning promises', function () {
beforeEach(function () {
ctrl = controller('AuthCtrl', {
$scope: scope,
myService: myService
});
ctrl.initController();
});
it('shows another form to validate login process', function () {
ctrl.login();
deferred.resolve(mockResponseData.response1);
scope.$digest();
expect(ctrl.invalidCredentials).toBe(false);
expect(ctrl.needForm).toEqual('2sAuth');
expect(ctrl.dataLoading).toBe(false);
});
});
});
})();
Since in my factory almost every method requires data and callback I've created mockHttp functions which takes those arguments and deferred promise. In it block I simply call need function, resolve promise with my prepared answers mock and check my expectations. Everything work. Thanks to for aiming in wich way to look
I wrote a page that allows me to change my password. The code works and it does everything I want it to do, so I started writing tests. Since I'm not as experienced in Angular testing this had proven to be quite difficult and I can't get passed this error:
TypeError: 'undefined' is not an object (evaluating 'plan.apply')
at /Users/denniegrondelaers/asadventure/myproject-web/src/users/controllers/userPasswordController.js:9
at /Users/denniegrondelaers/asadventure/myproject-web/test/unitTests/specs/users/controllers/userPasswordControllerSpec.js:98
The controller:
userPasswordController.js
users.controllers.controller('userPasswordController',
['$scope', 'Session', '$state', 'UserService', 'languages',
function ($scope, Session, $state, UserService, languages) {
$scope.languages = languages;
$scope.password = "";
$scope.notEqual = false;
$scope.isSuccessful = false;
$scope.changePassword = function() {
var pw = {
userId: Session.getCurrentSession().userId,
oldPassword: encrypt($scope.password.oldPassword),
newPassword: encrypt($scope.password.newPassword),
newPasswordRepeat: encrypt($scope.password.newPasswordRepeat)
};
if (pw.newPassword === pw.newPasswordRepeat) {
$scope.notEqual = false;
UserService.setNewPassword(pw).then(function(res) {
$scope.formErrors = undefined;
$scope.isSuccessful = true;
}, function (error) {
$scope.formErrors = error.data;
}
);
} else {
$scope.notEqual = true;
}
};
var encrypt = function (password) {
var encrypted = CryptoJS.md5(password);
return encrypted.toString(CryptoJS.enc.Hex);
};
}
]
);
The service:
userService.js
userService.setNewPassword = function (password) {
return $http
.put(EnvironmentConfig.endpointUrl +
"/password/change", password)
};
The test:
userPasswordControllerSpec.js
describe('Users', function () {
describe('Controllers', function () {
fdescribe('userPasswordController', function () {
var $scope,
controller,
$q,
willResolve,
mockSession,
mockState,
mockUserService,
mockLanguages;
beforeEach(function () {
module('mysite.users.controllers');
module(function ($provide) {
$provide.value('translateFilter', function (a) {
return a;
});
$provide.value('$state', function (a) {
return a;
});
});
mockSession = {
getCurrentSession: function () {
return {userId: 4};
}
};
mockState = {
params: {
id: 1
},
go: function () {
}
};
mockLanguages = {
getLanguages : function () {
var deferred = $q.defer();
deferred.resolve({
data: [{}]
});
return deferred.promise;
}
};
mockUserService = {
setNewPassword : function () {
var deferred = $q.defer();
if (willResolve) {
deferred.resolve({
data: [{}]
});
}
return deferred.promise;
}
};
inject(function (_$q_, $controller, $rootScope) {
controller = $controller;
$q = _$q_;
$scope = $rootScope.$new();
});
controller('userPasswordController', {$scope: $scope, Session: mockSession, $state: mockState,
UserService: mockUserService, languages: mockLanguages
});
willResolve = true;
});
it('should change password', function () {
spyOn(mockUserService, 'setNewPassword').and.callThrough();
spyOn(mockState, 'go').and.callThrough();
spyOn(mockSession, 'getCurrentSession').and.callFake();
expect(mockUserService.setNewPassword).not.toHaveBeenCalled();
expect($scope.isSubmitable()).not.toBeTruthy();
$scope.compareStoreSelection = function () {
return true;
};
$scope.password = {
oldPassword: "123456",
newPassword: "password",
newPasswordRepeat: "password"
};
expect($scope.isSubmitable()).toBeTruthy();
>>> $scope.changePassword(); <<< LOCATION OF ERROR, line 98
expect(mockUserService.setNewPassword).toHaveBeenCalled();
$scope.$apply();
});
});
});
});
I've marked the line that gives the code in the test.
Anybody any idea how to fix this? A colleague suggested altering my controller code, but I'd like to keep it as it is, since it seems logical that this code shouldn't be altered for testing to work, right?
Solution
Yarons' suggestion to change the mockSession.getCurrentSession.callFake to mockSession.getCurrentSession.callThrough fixed it!
code:
$scope.nextStep = function(route) {
session.save($scope.sessionViewModel);
var input = {
emailAddress : session.account.email,
caller : 'USERNAME_EXIST'
};
webServiceDal.doesWebLoginExist(input).success(function(response) {
console.log(response.WebLoginAppResponse.errorFlag);
if ((response.WebLoginAppResponse.errorFlag) && ((response.WebLoginAppResponse.returnCode == 1006) || (response.WebLoginAppResponse.returnCode == 'MSG0307'))) {
$scope.globalError = $scope.validationViewModel.email.existErrorMessage;
}
else
$location.path(route);
});
};
test:
describe('forgotPasswordCtrl', function() {
beforeEach(module('forgotPasswordApp'));
var scope, controller, q, $location, route, deferred, mockSessionService, validationProviderMock, webServDalMock;
beforeEach(function(){
var config = {
urlPath : {
match : ""
}
};
validationProviderMock = {
};
var response = {
};
mockSessionService = {
account : {
email : ""
},
clear : function(){
return true;
}
};
webServDalMock = {
forgotPassword : function(){
deferred = q.defer();
deferred.resolve(response);
return deferred.promise;
},
doesWebLoginExist : function(){
deferred = q.defer();
deferred.resolve(response);
return deferred.promise;
}
};
spyOn(webServDalMock, 'forgotPassword').and.callThrough();
spyOn(webServDalMock, 'doesWebLoginExist').and.callThrough();
spyOn(mockSessionService, 'clear').and.callThrough();
});
beforeEach(inject(function($rootScope, $controller, _$location_, $q){
scope = $rootScope.$new();
$location = _$location_;
q = $q;
controller = $controller('forgotPasswordCtrl', { $scope: scope, webServiceDal : webServDalMock, session : mockSessionService, validationProvider : validationProviderMock });
scope.$apply();
}));
it('should call clear method of session', function(){
scope.cancel();
expect(mockSessionService.clear).toHaveBeenCalled();
});
it('should return the correct url', function(){
scope.cancel();
config.urlPath.match("tfgm_customer");
expect(window.location.assign).toEqual("/web/tfgm_customer");
});
it('asf', function(){
scope.cancel();
config.urlPath.match("tfgm_customerERROR");
expect(window.location.assign).toEqual("/web/tfgm_admin");
});
it('should call webServiceDal', function(input){
scope.finish();
scope.$apply();
expect(webServDalMock.forgotPassword).toHaveBeenCalled();
});
it('should call webServiceDal', function(){
scope.nextStep(route);
scope.$apply();
expect(webServDalMock.doesWebLoginExist).toHaveBeenCalled();
});
});
before each:
beforeEach(inject(function($rootScope, $controller, _$location_, $q){
scope = $rootScope.$new();
$location = _$location_;
q = $q;
controller = $controller('forgotPasswordCtrl', { $scope: scope, webServiceDal : webServDalMock, session : mockSessionService, validationProvider : validationProviderMock });
scope.$apply();
}));
cant work out for the life of me why this is not passing? i have called the correct function and the called the expect correctly. i have other files which i have run identical tests on, the only difference is the naming of variables etc. and they pass.
am i missing something simple?
You problem is that a deferred promise does not return a success function but rather (then, catch or finally), $q docs
You would have to modify your mock doesWebLoginExist to return a success function when called.
EDIT:
Something like
doesWebLoginExist : function(){
return {success: function(cb) {
cb(response);
}};
}
I've tried numerous different ways of writing a unit test for an AngularJS service that calls angular-translate, and I just can't get it to work out. Any advice would be appreciated. Here's my most promising example:
(function() {
var app = angular.module("theApp", ["pascalprecht.translate"]);
var theService = function($translate) {
var theFunction = function(data) {
return $translate("FOO", { input: data.in }).then(function(trans) {
data.out = trans;
});
};
return {
theFunction: theFunction
};
};
app.factory("theService", ["$translate", theService]);
}());
describe("theService", function() {
beforeEach(module("theApp", function($translateProvider, $provide) {
$translateProvider.useLoader('customLoader');
$provide.service('customLoader', function($q) {
return function() {
var deferred = $q.defer();
deferred.resolve({
"FOO": "foo {{input}}"
});
return deferred.promise;
};
});
}));
it("function translates input", inject(function($rootScope, theService) {
var data = { in: "bar", out: "fail" };
theService.theFunction(data);
$rootScope.$apply();
expect(data.out).toBe("foo bar");
}));
});
A JSFiddle can be found here: http://jsfiddle.net/danBhentschel/q71r874t/
Okay. I guess I figured it out on my own. I started out with the test found here:
https://github.com/angular-translate/angular-translate/blob/master/test/unit/service/translate.spec.js#L409
And I was able to slowly morph this passing test into what I wanted to do:
(function() {
var app = angular.module("theApp", ["pascalprecht.translate"]);
var theService = function($translate) {
var theFunction = function(data) {
return $translate("FOO", { input: data.in }).then(function(trans) {
data.out = trans;
});
};
return {
theFunction: theFunction
};
};
app.factory("theService", ["$translate", theService]);
}());
describe("theService", function() {
var $rootScope;
beforeEach(module("theApp", function($translateProvider, $provide) {
$translateProvider.useLoader("customLoader");
$provide.service("customLoader", function($q) {
return function() {
var deferred = $q.defer();
deferred.resolve({
"FOO": "foo {{input}}"
});
return deferred.promise;
};
});
}));
beforeEach(inject(function ($translate, _$rootScope_) {
$rootScope = _$rootScope_;
$translate.use("en_US");
$rootScope.$apply();
}));
it("function translates input", inject(function(theService) {
var data = { in: "bar", out: "fail" };
theService.theFunction(data);
$rootScope.$apply();
expect(data.out).toBe("foo bar");
}));
});
A JSFiddle with the solution can be found here: http://jsfiddle.net/danBhentschel/yLt3so14/
Please feel free to point out any stupid mistakes I made. I'm still kinda new at this.