AngularJS jasmine promise testing fails because of timeout - javascript

I'm trying to test my login controller, which looks like this:
describe('LoginController', function() {
beforeEach(module('task6'));
var $controller, LoginService;
beforeEach(inject(function(_$controller_, _LoginService_) {
$controller = _$controller_;
LoginService = _LoginService_;
}));
describe('LoginController.submitLogin', function() {
it('tests if such user exists', function(done) {
var $scope = {};
var controller = $controller('LoginController', {$scope: $scope});
var resultValue;
controller.loginField = 'John';
controller.password = 'Smith';
LoginService.signIn(controller.loginField,
controller.password)
.then(function() {
expect(true).toBe(true);
done();
});
});
});
});
Where signIn function is:
function signIn(loginField, password) {
var defer = $q.defer();
if (loginField && password) {
defer.resolve("nice one");
} else {
defer.reject("oh dear");
}
return defer.promise;
}
But it always fails with "Async callback was not invoken within timeout specified..."

You need to call $scope.$digest() once at the end of your test. A promise will never be resolved within the same digest cycle it has been created in.
Explanation
While it would be possible for the framework to allow this, the developers chose not to. See this example while it could be problematic:
function login(login, password) {
LoginService.signIn(login, password)
.then(function() {
cancelLoadingAnimation();
});
startLoadingAnimation();
}
Usually a promise is resolved asynchronously, so we don't have a problem here. The loading animation is started in the login function and cancelled when signing in succeeded. But imagine the promise was instantly resolved (for instance in a test like yours)! It would now be possible that the animation was cancelled before it was even started.
Of course this can be cured by moving the call of startLoadingAnimation() above the call to signIn(). However, it is much easier to reason about your code when promises are always resolved asynchronously.
Update: As #gnerkus states in his answer, you have to create the $scope as a child of the $rootScope. However, this alone will not solve the problem. You will have to do both.

You need to create the scope object for the controller as an instance of $rootScope:
describe('LoginController', function() {
beforeEach(module('task6'));
var $controller, LoginService;
// Inject the $rootScope service for use in creating new scope objects.
beforeEach(inject(function($rootScope, _$controller_, _LoginService_) {
$controller = _$controller_;
LoginService = _LoginService_;
}));
describe('LoginController.submitLogin', function() {
it('tests if such user exists', function(done) {
// Create a new scope for the controller.
var scope = $rootScope.$new();
var controller = $controller('LoginController', {$scope: scope});
var resultValue;
controller.loginField = 'John';
controller.password = 'Smith';
LoginService.signIn(controller.loginField,
controller.password)
.then(function() {
expect(true).toBe(true);
done();
});
});
});
});

I guess, #gnerkus and #lex82 were both right - I need to run $digest cycle for promises, but I still need a reference to a real scope to do this. Here is the final, working version of my code:
describe('LoginController', function() {
beforeEach(module('task6'));
var $rootScope, $controller, LoginService;
beforeEach(inject(function(_$rootScope_, _$controller_, _LoginService_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
LoginService = _LoginService_;
}));
describe('LoginController.submitLogin', function() {
it('tests if such user exists', function(done) {
var $scope = $rootScope.$new();
var controller = $controller('LoginController',
{$scope: $scope});
controller.loginField = 'John';
controller.password = 'Smith';
LoginService.signIn(controller.loginField,
controller.password)
.then(function(logged) {
expect(true).toBe(true);
done();
})
$scope.$digest();
});
});
});
Thank you, guys!

Related

AngularJS testing controller with jasmine and spyOn gives error 'method undefined'

So I am writing unit tests for an application in angularJS using Jasmine.
I have a controller with an "init" method which calls "secondMethod" and "thirdMethod"
I want to test with a jasmine spyOn whether "secondMethod" is called correctly.
My controller looks like this:
function init() {
secondMethod().then(function () {
thirdMethod();
});
}
init();
function secondMethod(){
//do something
}
function thirdMethod(){
//do something
}
and my test file looks like this:
describe("nameOfTheController", function () {
var $rootScope,
$controller,
$scope,
controller;
beforeEach(function () {
angular.mock.module("myModule");
inject(function (_$controller_, _$rootScope_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
$scope = $rootScope.$new();
controller = $controller('nameOfTheController', {
'$scope': $scope
});
});
});
describe("init", function(){
it('should run secondMethod', function(){
spyOn(controller, 'secondMethod');
expect(controller.secondMethod).toHaveBeenCalled();
});
it('should run thirdMethod', function(){
spyOn(controller, 'thirdMethod');
expect(controller.thirdMethod).toHaveBeenCalled();
});
As you can see I inject the controller in beforeEach but I get error that method "secondMethod" and "thirdMethod" are not defined and I am not quite sure why.
I have also tried doing something like the following but to no avail
controller:
var vm = this;
vm.init = function() {
vm.secondMethod().then(function () {
vm.thirdMethod();
});
}
vm.init();
vm.secondMethod = function(){
//do something
}
vm.thirdMethod = function(){
//do something
}
testfile:
describe("nameOfTheController", function () {
var $rootScope,
$controller,
$scope,
controller;
beforeEach(function () {
angular.mock.module("myModule");
inject(function (_$controller_, _$rootScope_) {
$rootScope = _$rootScope_;
$controller = _$controller_;
$scope = $rootScope.$new();
controller = $controller('nameOfTheController', {
'$scope': $scope
});
});
});
describe("init", function(){
it('should run secondMethod', function(){
spyOn(controller, 'secondMethod');
expect(controller.secondMethod).toHaveBeenCalled();
});
it('should run thirdMethod', function(){
spyOn(controller, 'thirdMethod');
expect(controller.thirdMethod).toHaveBeenCalled();
});
Does anyone know why second and third method are undefined?
EDIT:
The reason why second and third method returned undefined when prefixing with "vm." was that the init function was called before the second and third method had been defined.
Moving the call of init to be below the second and third method definitions solved the problem. Now I just have the problem that the spy expects the method to be called but it doesn't get called
var vm = this;
vm.init = function() {
vm.secondMethod().then(function () {
vm.thirdMethod();
});
}
vm.secondMethod = function(){
//do something
}
vm.thirdMethod = function(){
//do something
}
vm.init();
The controller is initiated in beforeEach(), and that is when you init() => seconMethod(), while you spy on it only in the it() block.
On the other hand, you can't spy before, since you won't have the controller object.
IMO, the solution will be to revise your code and call init() explicitally:
it('should run secondMethod', function() {
spyOn(controller, 'secondMethod');
expect(controller.secondMethod).not.toHaveBeenCalled();
controller.init();
expect(controller.secondMethod).toHaveBeenCalled();
});
https://jsfiddle.net/ronapelbaum/v9vyLpws/

Karma-Jasmine: How to properly spy on a Modal?

THE SITUATION:
I am unit testing my Angular / Ionic app.
I am having troubles with the modal.
At the moment i can test that the modal has been called. That's all so far. I cannot test the proper show() and hide() method of the modal.
I am getting the following errors:
TypeError: $scope.modal_login.show is not a function
Error: show() method does not exist
TypeError: $scope.modal_login.hide is not a function
Error: hide() method does not exist
I think it depends entirely on the spy. I don't know how to properly spy on the modal, and i think that once that is done, everything will work fine.
THE CODE:
The controller:
$scope.open_login_modal = function()
{
var temp = $ionicModal.fromTemplateUrl('templates/login.html',{scope: $scope});
temp.then(function(modal) {
$scope.modal_login = modal;
$scope.modal_login.show();
$scope.for_test_only = true;
});
};
$scope.close_login_modal = function()
{
$scope.modal_login.hide();
};
Note: the code of open_login_modal function has been refactored to facilitate the test. The original code was:
$scope.open_login_modal = function()
{
$ionicModal.fromTemplateUrl('templates/login.html', {
scope: $scope
}).then(function(modal) {
$scope.modal_login = modal;
$scope.modal_login.show();
});
};
The test:
describe('App tests', function()
{
beforeEach(module('my_app.controllers'));
function fakeTemplate()
{
return {
then: function(modal){
$scope.modal_login = modal;
}
}
}
beforeEach(inject(function(_$controller_, _$rootScope_)
{
$controller = _$controller_;
$rootScope = _$rootScope_;
$scope = _$rootScope_.$new();
$ionicModal =
{
fromTemplateUrl: jasmine.createSpy('$ionicModal.fromTemplateUrl').and.callFake(fakeTemplate)
};
var controller = $controller('MainCtrl', { $scope: $scope, $rootScope: $rootScope, $ionicModal: $ionicModal });
}));
describe('Modal tests', function()
{
beforeEach(function()
{
$scope.open_login_modal();
spyOn($scope.modal_login, 'show'); // NOT WORKING
spyOn($scope.modal_login, 'hide'); // NOT WORKING
});
it('should open login modal', function()
{
expect($ionicModal.fromTemplateUrl).toHaveBeenCalled(); // OK
expect($ionicModal.fromTemplateUrl.calls.count()).toBe(1); // OK
expect($scope.modal_login.show()).toHaveBeenCalled(); // NOT PASS
expect($scope.for_test_only).toEqual(true); // NOT PASS
});
it('should close login modal', function()
{
$scope.close_login_modal();
expect($scope.modal_login.hide()).toHaveBeenCalled(); // NOT PASS
});
});
});
As you can see from the code $scope.for_test_only it should be equal to true but is not recognized. I get this error message instead:
Expected undefined to equal true.
The same happens to the show() and hide() method. They are not seen by the test.
And i think because they are not declared in the spy.
THE QUESTION:
How can i properly spy on a modal?
Thank you very much!
The question here could be extrapolated to how to properly spy on a promise. You are very much on the right track here.
However, if you want to test that whatever your callback to the success of the promise is called, you have to execute two steps:
Mock the service (in your case $ionicModal) and return some fake function
In that fake function, execute the callback that is passed to you by the production code.
Here is an illustration:
//create a mock of the service (step 1)
var $ionicModal = jasmine.createSpyObj('$ionicModal', ['fromTemplateUrl']);
//create an example response which just calls your callback (step2)
var successCallback = {
then: function(callback){
callback.apply(arguments);
}
};
$ionicModal.fromTemplateUrl.and.returnValue(successCallback);
Of course, you can always use $q if you don't want to be maintaining the promise on your own:
//in your beforeeach
var $ionicModal = jasmine.createSpyObj('$ionicModal', ['fromTemplateUrl']);
//create a mock of the modal you gonna pass and resolve at your fake resolve
var modalMock = jasmine.createSpyObj('modal', ['show', 'hide']);
$ionicModal.fromTemplateUrl.and.callFake(function(){
return $q.when(modalMock);
});
//in your test
//call scope $digest to trigger the angular digest/apply lifecycle
$scope.$digest();
//expect stuff to happen
expect(modalMock.show).toHaveBeenCalled();

Can't access $rootScope from another controller

Why can't I access the $rootScope, particulary the currentUser object and the signedIn() function from the RegistrationController?
I'm trying to follow a tutorial example and I'm able to sucessfully set the $rootScope.currentUser variable in a service (Authentication), but when I try to access it from another controller (RegistrationController) I cannot access it.
My understanding is that $rootScope is kind of a global variable that is accessible from all the app, is this correct?
myApp.controller('RegistrationController',
function($scope, $firebaseAuth, $location, Authentication, $rootScope){
$scope.login = function() {
Authentication.login($scope.user)
.then(function(userReturned){
console.log('registration.js: logged in user '+userReturned.uid);
//console.log('registration.js: $rootScope.currentUser ahora es... ');
//console.log($rootScope.currentUser);
$location.path('/meetings');
})
.catch(function(error) {
$scope.message = error.toString();
});
} //login
}); //RegistrationController
myApp.factory('Authentication',
function($firebase, $firebaseAuth, FIREBASE_URL, $location, $rootScope) {
// using $firebaseAuth instead of SimpleLogin
var ref = new Firebase(FIREBASE_URL);
var authObj = $firebaseAuth(ref);
var myObject = {
login : function(user) {
return authObj.$authWithPassword({
email: user.email,
password: user.password
})
.then(function(authData){
console.log('authentication.js: logged in user '+ authData.uid);
var userRef = new Firebase(FIREBASE_URL + 'users/' + authData.uid);
var userObj = $firebase(userRef).$asObject();
userObj.$loaded().then(function() {
$rootScope.currentUser = userObj;
});
$rootScope.$broadcast('$firebaseAuth:authWithPassword',authData); // avisa al scope
return authData;
});
}, //login
signedIn: function() {
//console.log(authObj);
//console.log('authentication.js: signedIn function called and returned '+ (authObj.user != null) );
return authObj.user != null;
} // signedIn
} //myObject
// add signedIn to the $rootScope
$rootScope.signedIn = function() {
return myObject.signedIn();
}
return myObject;
});
I believe what's happening is the promise Authentication.login is being resolved before currentUser being set $rootScope.currentUser = userObj;
to make sure this is the case, try put a breakpoint on this line in your Authentication service:
$rootScope.currentUser = userObj;
and another breakpoint on this one in your controller:
console.log($rootScope.currentUser);
and see which one is being executed before the other.
IF this is the case, try this:
Move the block of code in [THEN] statement from your service to your controller, where you are treating the current user and logging it.
It looks like you're trying to print $rootScope.currentUser from RegistrationController.login(). But currentUser was never set on $rootScope. Here's an example that demonstrates using $rootScope on two controllers.
angular.module('Main', [])
.controller("SetCtrl", function($scope, $rootScope) {
$scope.name = 'abc';
$rootScope.copy = $scope.name;
})
.controller("GetCtrl", function($scope, $rootScope) {
$scope.fromSetCtrl = $rootScope.copy;
});
http://plnkr.co/edit/dj6Y9hIEJ3KA9yAjuQP5
I'm not exactly sure how your are using your service/factory and how your apps and controllers are set up.
I think $rootScope is the "top" scope of the outermost controller of your app. You can have a $rootScope for each of your "app".
I would guess that if you set a variable in the $rootScope in the service in one of your app and then try to access your service from the $rootScope from the controller from ANOTHER app, you will not be able to find the variable because it is in another $rootScope. In that sense $rootScope is NOT a global variable.
You can access $rootScope variables directly from the child controllers if you pass in "$rootScope" when you define your controller, or from indirectly from $scope of the child controllers, because angular will look for the variable from the current controller's scope all the way up to that controller's $rootScope.
Hope this helps.

Angular Jasmine - mock service (angularjs-rails-resource) - TypeError: object is not a function

This is Rails 4.0 App with angular-rails gem and jasmine gem. I also use angularjs-rails-resource.
I have simple controller:
app.controller('new', function ($scope, Appointment, flash) {
$scope.appointment = new Appointment();
$scope.attachments = [];
$scope.appointment.agendas_attributes = [];
$scope.createAppointment = function(){
$scope.appointment.attachments = $scope.attachments;
$scope.appointment.create().then(function(data){
flash(data.message);
}, function(error){
flash('error', error.data.message);
$scope.appointment.$errors = error.data.errors;
});
};
And on the unit test in Jasmine i want to isolate dependencies with jasmine.createSpyObj:
describe("Appointment new controller",function() {
var $scope, controller, mockAppointment, mockFlash, newCtrl;
beforeEach(function() {
module("appointments");
inject(function(_$rootScope_, $controller) {
$scope = _$rootScope_.$new();
$controller("new", {
$scope: $scope,
Appointment: jasmine.createSpyObj("Appointment", ["create"])
});
});
});
});
but i get an error:
TypeError: object is not a function
on line:
Appointment: jasmine.createSpyObj("Appointment", ["create"])
Can anybody help ? :-)
I believe the error is being thrown on this line:
$scope.appointment = new Appointment();
Appointment is an object literal, and not a function, so essentially you're trying to do this:
var x = {create: function(){}};
var y = new x();
But what you seem to want to do is this:
var x = function(){return {create: function(){}}};
var y = new x();
So make your mock like this:
Appointment: function() {
return jasmine.createSpyObj("Appointment", ["create"])
}
I see someone beat me to the answer;)
I have one suggestion that will make your describe block look cleaner
The module and inject statements can be nested inside the beforeEach definition:
describe("Appointment new controller", function () {
var $scope, controller, mockAppointment, mockFlash, newCtrl;
beforeEach(module("appointments"));
beforeEach(inject(function (_$rootScope_, $controller) {
$scope = _$rootScope_.$new();
$controller("new", {
$scope: $scope,
Appointment: function () {
return jasmine.createSpyObj("Appointment", ["create"])
}
});
}));
}

Jasmine angularjs - spying on a method that is called when controller is initialized

I am currently using Jasmine with Karma(Testacular) and Web Storm to write unit test. I am having trouble spying on a method that gets called immediately when the controller is initialized. Is it possible to spy on a method that is called when the controller is initialized?
My controller code, the method I am attempting to spy on is getServicesNodeList().
myApp.controller('TreeViewController', function ($scope, $rootScope ,$document, DataServices) {
$scope.treeCollection = DataServices.getServicesNodeList();
$rootScope.viewportHeight = ($document.height() - 100) + 'px';
});
And here is the test spec:
describe("DataServices Controllers - ", function () {
beforeEach(angular.mock.module('myApp'));
describe("DataServicesTreeview Controller - ", function () {
beforeEach(inject(function ($controller, $rootScope, $document, $httpBackend, DataServices) {
scope = $rootScope.$new(),
doc = $document,
rootScope = $rootScope;
dataServices = DataServices;
$httpBackend.when('GET', '/scripts/internal/servicedata/services.json').respond(...);
var controller = $controller('TreeViewController', {$scope: scope, $rootScope: rootScope, $document: doc, DataServices: dataServices });
$httpBackend.flush();
}));
afterEach(inject(function($httpBackend){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
}));
it('should ensure DataServices.getServicesNodeList() was called', inject(function ($httpBackend, DataServices) {
spyOn(DataServices, "getServicesNodeList").andCallThrough();
$httpBackend.flush();
expect(DataServices.getServicesNodeList).toHaveBeenCalled();
}));
});
});
The test is failing saying that the method has not been called. I know that I should mock the DataServices and pass that into the test controller. But it seems like I would still have the same problem when spying on that method whether it is a mock or not. Anyone have any ideas or could point me to resources on the correct way to handle this?
When writing unit tests, you should isolate each piece of code. In this case, you need to isolate your service and test it separately. Create a mock of the service and pass it to your controller.
var mockDataServices = {
getServicesNodeList: function () {
return <insert your sample data here > ;
}
};
beforeEach(inject(function ($controller, $rootScope, $document) {
scope = $rootScope.$new(),
doc = $document,
rootScope = $rootScope;
var controller = $controller('TreeViewController', {
$scope: scope,
$rootScope: rootScope,
$document: doc,
DataServices: mockDataServices
});
}));
If it is your service that is making the $http request, you can remove that portion from your unit controller test. Write another unit test that tests that the service is making the correct http calls when it is initialized.

Categories