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

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();

Related

Spying on recursive Angular controller method

I have a recursive method that, if a flag is set, will call itself every five seconds. I'm trying to write a test that spies on the method, calls it, waits six seconds and then expects the method to have been called twice. My test fails, as the spy reports the method only being called once (the initial call).
I'm using the Angular style guide, so am attaching these methods to a placeholder for this. I suspect there may be an issue with scoping of the controller returned from angular-mocks $controller(), but I'm not sure—most people are attaching methods to $scope.
Without attaching methods to $scope, how can I create a spy to verify that my method has been called twice?
app.js:
'use strict';
angular
.module('MyApp', [
//...
]);
angular
.module('MyApp')
.controller('MyController', MyController);
MyController.$inject = [
//...
];
function MyController() {
var vm = this;
vm.callMyself = callMyself;
vm.flag = false;
function callMyself () {
console.log('callMyself');
if (vm.flag) {
console.log('callMyself & flag');
setTimeout(vm.callMyself, 5000);
}
}
}
appSpec.js:
describe('MyController', function () {
var $scope;
beforeEach(function () {
module('MyApp');
});
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
controllerInstance = $controller('MyController', {$scope: $scope});
}));
it('should call itself within 6 seconds if flag is true', function (done) {
controllerInstance.flag = true;
spyOn(controllerInstance, 'callMyself');
controllerInstance.callMyself();
setTimeout(function () {
expect(controllerInstance.callMyself).toHaveBeenCalledTimes(2);
done();
}, 6000);
}, 7000);
});
Working Plunker
You need to use .and.callThrough() to further execute the function that would call itself:
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.
spyOn(controllerInstance, 'callMyself').and.callThrough();
Tested in the plunker - it works.

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/

How do you mock an angularjs $resource factory

I have a resource factory
angular.module('mean.clusters').factory('Clusters', ['$resource',
function($resource) {
return $resource('clusters/:clusterId/:action', {
clusterId: '#_id'
}, {
update: {method: 'PUT'},
status: {method: 'GET', params: {action:'status'}}
});
}]);
and a controller
angular.module('mean.clusters').controller('ClustersController', ['$scope',
'$location', 'Clusters',
function ($scope, $location, Clusters) {
$scope.create = function () {
var cluster = new Clusters();
cluster.$save(function (response) {
$location.path('clusters/' + response._id);
});
};
$scope.update = function () {
var cluster = $scope.cluster;
cluster.$update(function () {
$location.path('clusters/' + cluster._id);
});
};
$scope.find = function () {
Clusters.query(function (clusters) {
$scope.clusters = clusters;
});
};
}]);
I am writing my unit tests and every example I find is using some form of $httpBackend.expect to mock the response from the server, and I can do that just fine.
My problems is, when unit testing my controller functions I would like to mock the Clusters object. If I'm using $httpBackend.expect, and I introduce a bug in my factory every unit test in my controller will fail.
I would like to have my test of $scope.create test only $scope.create and not also my factory code.
I've tried adding a provider in the beforeEach(module('mean', function ($provide) { part of my tests but I cant seem to get it right.
I also tried
clusterSpy = function (properties){
for(var k in properties)
this[k]=properties[k];
};
clusterSpy.$save = jasmine.createSpy().and.callFake(function (cb) {
cb({_id: '1'});
});
and setting Clusters = clusterSpy; in the before(inject but in the create function, the spy gets lost with
Error: Expected a spy, but got Function.
I have been able to get a spy object to work for the cluster.$update type calls but then it fails at var cluster = new Clusters(); with a 'not a function' error.
I can create a function that works for var cluster = new Clusters(); but then fails for the cluster.$update type calls.
I'm probably mixing terms here but, is there a proper way to mock Clusters with spies on the functions or is there a good reason to just go with $httpBackend.expect?
Looks like I was close a few times but I think I have it figured out now.
The solution was the 'I also tried' part above but I was not returning the spy object from the function.
This works, it can be placed in either the beforeEach(module( or beforeEach(inject sections
Step 1: create the spy object with any functions you want to test and assign it to a variable that's accessible to your tests.
Step 2: make a function that returns the spy object.
Step 3: copy the properties of the spy object to the new function.
clusterSpy = jasmine.createSpyObj('Clusters', ['$save', 'update', 'status']);
clusterSpyFunc = function () {
return clusterSpy
};
for(var k in clusterSpy){
clusterSpyFunc[k]=clusterSpy[k];
}
Step 4: add it to the $controller in the beforeEach(inject section.
ClustersController = $controller('ClustersController', {
$scope: scope,
Clusters: clusterSpyFunc
});
inside your tests you can still add functionality to the methods using
clusterSpy.$save.and.callFake(function (cb) {
cb({_id: '1'});
});
then to check the spy values
expect(clusterSpy.$save).toHaveBeenCalled();
This solves both problems of new Clusters() and Clusters.query not being a function. And now I can unit test my controller with out a dependency on the resource factory.
Another way to mock the Clusters service is this:
describe('Cluster Controller', function() {
var location, scope, controller, MockClusters, passPromise, q;
var cluster = {_id : '1'};
beforeEach(function(){
// since we are outside of angular.js framework,
// we inject the angujar.js services that we need later on
inject(function($rootScope, $controller, $q) {
scope = $rootScope.$new();
controller = $controller;
q = $q;
});
// let's mock the location service
location = {path: jasmine.createSpy('path')};
// let's mock the Clusters service
var MockClusters = function(){};
// since MockClusters is a function object (not literal object)
// we'll need to use the "prototype" property
// for adding methods to the object
MockClusters.prototype.$save = function(success, error) {
var deferred = q.defer();
var promise = deferred.promise;
// since the Clusters controller expect the result to be
// sent back as a callback, we register the success and
// error callbacks with the promise
promise.then(success, error);
// conditionally resolve the promise so we can test
// both paths
if(passPromise){
deferred.resolve(cluster);
} else {
deferred.reject();
}
}
// import the module containing the Clusters controller
module('mean.clusters')
// create an instance of the controller we unit test
// using the services we mocked (except scope)
controller('ClustersController', {
$scope: scope,
$location: location,
Clusters: MockClusters
});
it('save completes successfully', function() {
passPromise = true;
scope.save();
// since MockClusters.$save contains a promise (e.g. an async call)
// we tell angular to process this async call before we can validate
// the response
scope.$apply();
// we can call "toHaveBeenCalledWith" since we mocked "location.path" as a spy
expect(location.path).toHaveBeenCalledWith('clusters/' + cluster._id););
});
it('save doesn''t complete successfully', function() {
passPromise = false;
scope.save();
// since MockClusters.$save contains a promise (e.g. an async call)
// we tell angular to process this async call before we can validate
// the response
scope.$apply();
expect(location.path).toHaveBeenCalledWith('/error'););
});
});
});

Testng a partally executed $timeout in AngularJS

I have an AngularJS service. This service uses a $timeout in its function. I'm trying to figure out how to test this service in various situations. Here is my code:
myApp.factory('myService', ['$timeout', function($timeout) {
var isRunning = false;
var myTimer = null;
return {
myTimer: myTimer,
isRunning: isRunning,
execute: function(time) {
this.isRunning = true;
this.myTimer = $timeout(
function() {
this.isRunning= false;
this.myTimer= null;
},
time
);
}
};
}]);
Now, I'm trying to test this code. I have the following written:
describe('myService', function () {
var myService = null;
var $timeout = null;
var $rootScope = null;
beforeEach(inject(function (_$timeout_, _$rootScope_, _myService_) {
myService = _myService_;
$timeout = _$timeout_;
$rootScope = _$rootScope_;
}));
it('should run for one second', function() {
myService.execute(1000);
$timeout.flush();
$rootScope.$digest();
expect(myService.isRunning).toBe(false);
});
});
The test "works". However, if 750 milliseonds have elapsed, I would expect myService.isRunning to be true. However, I do not know how to test for that scenario. Can someone please show me how to test tht situation? thank you
You can imitate a certain amount of time passing with the $timeout.flush method. The method takes an optional parameter called delay. It's explained in the documentation here. With that in mind, you could write a test that looks like this:
it('should run for one second', function() {
myService.execute(1000);
$timeout.flush(750);
$rootScope.$digest();
expect(myService.isRunning).toBe(true);
});

How do I test that this nested function was called in an AngularJS unit test?

I'm not new to unit testing (C#) but I am very new to unit testing in AngularJS.
I'm trying to test my controller and so far have been able to get several tests to work properly, however there are some that are proving to be rather difficult.
I have $scope method that makes a call to our Authentication service which returns a promise. In the "then" function I am checking to see if the user was indeed authenticated and based on that I will call a private function that will go out and make other Service calls.
Currently the test is failing with the following error:
Expected spy getConfigurationStatuses to have been called.
Error: Expected spy getConfigurationStatuses to have been called.
If anyone can please help point me in the right direction I would really appreciate it. I'll post the code below -Thanks for any help!
Here's my specs (the one that is not working is the "should call specific configurations if user is authenticated" spec:
describe('EnvironmentCtrl specs', function(){
var $rootScope = null, $scope = null, ctrl = null;
var Authentication = {
getCredentials: function(){ return true; }
};
var Environment = { getConfigurationStatuses: function(){ return true; } };
beforeEach(module('ngRoute'));
beforeEach(module('environment'));
beforeEach(module(function($provide){
$provide.value('SITE_ROOT', '/');
}));
beforeEach(inject(function(_$rootScope_, _$controller_, _$timeout_, _$location_, _$q_, _Authentication_, _Environment_){
$rootScope = _$rootScope_;
$controller = _$controller_;
$timeout = _$timeout_;
$location = _$location_;
$q = _$q_;
Authentication = _Authentication_;
Environment = _Environment_;
spyOn(Authentication, 'getCredentials').andCallThrough();
spyOn(Environment, 'getConfigurationStatuses').andCallThrough();
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
ctrl = $controller('EnvironmentCtrl', {$rootScope: $rootScope,$scope: $scope, $timeout: $timeout,
Eventor: {}, Controller: {}, Environment: Environment,Authentication: Authentication, ErrorService:{} });
}));
describe('When initializing the EnvironmentCtrl', function(){
// this one works fine!
it('should set default values on the scope object', function(){
expect($scope.controllerName).toEqual('EnvironmentCtrl');
expect($scope.environmentStatusType).toEqual('configurations');
expect($scope.configurationsSelected).toBe(true);
expect($scope.isDataLoaded).toBe(false);
});
// this works fine!
it('should make a call to authenticate the user', function(){
$scope.determineViewToDisplay();
expect(Authentication.getCredentials).toHaveBeenCalled();
});
// this one doesn't work!
it('should call specific configurations if user is authenticated', function(){
$scope.determineViewToDisplay();
$rootScope.isUserAuthenticated = true;
expect(Environment.getConfigurationStatuses).toHaveBeenCalled();
});
});
});
Here's the three functions that are involved in the unit tests:
$scope.determineViewToDisplay = function () {
Authentication.getCredentials().then(function(){
if ($rootScope.isUserAuthenticated === true) {
$scope.isAnonymous = false;
handleAuthenticatedUserView();
} else {
Eventor.publish('event:login', false);
$scope.isAnonymous = true;
handleAnonymousUserView();
}
}, function(err){
ErrorService.handleError(err, null, $scope.controllerName);
});
};
function handleAuthenticatedUserView() {
$scope.configurationStatusTimer = $timeout(function(){
displayConfigurationStatuses(true);
}, 5);
}
function displayConfigurationStatuses(isAuthenticated) {
Environment.getConfigurationStatuses(isAuthenticated).then(function(statuses){
setConfigurationsIconStatus(statuses);
$scope.configurationStatuses = statuses;
$scope.isDataLoaded = true;
amplify.store($rootScope.productCustomerName + '-configurationStatuses', statuses, {expires: 120000});
$rootScope.showLoadingIndicator = false;
}, function(err){
ErrorService.handleError(err, null, $scope.controllerName);
});
}
It looks like determineViewToDisplay() only calls handleAuthenticatedUserView if
$rootScope.isUserAuthenticated === true
but you're not setting $rootScope.isUserAuthenticated to true until after you call
$scope.determineViewToDisplay()
so handleAuthenticatedUserView() never gets called, and in turn displayConfigurationStatuses() never gets called.
I know this is an older question but...
Could it be the fact that the getCredentials method is actually an async operation? That is why you will see the 'should make a call to authenticate the user' pass, because the call to that function is synchronous. getCredentials returns a promise and keeps going. This causes your assertions to run before the resolve/reject handlers run (which is where your function under test is eventually called).
You can use the 'done' (or older 'runs/waitsFor') syntax in Jasmine with async operations to ensure your assertions aren't run until all promises have been resolved.
Also I just noticed that displayConfigurationStatuses is called within a timeout (more async). You may have to mock the $timeout service to execute immediately or maybe make handleAnonymousUserView return a promise that is resolved once the timeout executes.

Categories