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.
Related
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();
I have a controller that initializes an object upon initialization of the controller, and would like to test that it was called with the specific params that it is actually called with.
I know that I can test that $scope.autoSaveObj has certain properties that would tell me that it in fact initialized, but how would I spy on the initialization event itself?
Essentially, I want to spy on new autoSaveObj just like I would a method.
The main reason I want to test and spy on the object contructor is so that my karma-coverage plugin will show those lines as covered. It won't show the lines as covered if I just test the state of $scope.autoSaveObject after the initialization.
App.controller('ItemCtrl',[ '$scope', function($scope){
$scope.autoSaveObject = new autoSaveObj({
obj: $scope.item,
saveCallback: function() {
return $scope.saveItem();
},
errorCallback: null,
saveValidation: $scope.validItem,
delay: 2000
});
}]);
My guess is the code example is of a partial controller, because properties in the $scope are used which are not initialized here.
Since autoSaveObj is not defined anywhere, I assumed it is a global function. Consider moving this to a service or factory instead.
The following example shows how to
mock autoSaveObj
verify the call parameters, and
verify that the created instance is actually an instance of the correct type.
angular.module('myApp', []).
controller('ItemCtrl', function($scope, $window) {
// Use the injected $window object, so we don't rely on
// the environment and it can be mocked easily.
$scope.autoSaveObject = new $window.autoSaveObj({
obj: $scope.item,
saveCallback: function() {
return $scope.saveItem();
},
errorCallback: null,
saveValidation: $scope.validItem,
delay: 2000
});
});
describe('ItemCtrl', function() {
var $controller;
var $scope;
var $window;
var controller;
beforeEach(module('myApp', function($provide) {
$window = {
// Create an actual function that can be spied on.
// Using jasmine.createSpy won't allow to use it as a constructor.
autoSaveObj: function autoSaveObj() {}
};
// Provide the mock $window.
$provide.value('$window', $window);
}));
beforeEach(inject(function(_$controller_, $rootScope) {
$controller = _$controller_;
$scope = $rootScope.$new();
}));
it('should instantiate an autoSaveObj', function() {
spyOn($window, 'autoSaveObj');
// Initialize the controller in a function, so it is possible
// to do preparations.
initController();
// Do function call expectations as you would normally.
expect($window.autoSaveObj).toHaveBeenCalledWith(jasmine.objectContaining({
saveCallback: jasmine.any(Function),
delay: 2000
}));
// The autoSaveObject is an instance of autoSaveObj,
// because spyOn was used, not jasmine.createSpy.
expect($scope.autoSaveObject instanceof $window.autoSaveObj).toBe(true);
});
function initController() {
controller = $controller('ItemCtrl', {
$scope: $scope
});
}
});
<link href="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.css" rel="stylesheet"/>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/jasmine-html.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jasmine/2.2.1/boot.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular-mocks.js"></script>
I have some issues injecting my factory into testing spec. I am using requireJS to inject controllers and factories etc.
define(['controller', 'loginFactory', 'angular', 'angularMocks'],
function(ctrl, loginFactory, angular){
var scope,
OnBoardingCtrl;
describe('Controller: OnBoarding', function () {
beforeEach(angular.mock.inject(function ($rootScope, $controller, $location) {
angular.module('app');
scope = $rootScope.$new();
OnBoardingCtrl = $controller(ctrl, {
'$scope': scope,
'loginFactory': loginFactory,
});
}));
it('Should check endpoint', inject(function ($http, $httpBackend) {
var successCallback = jasmine.createSpy();
var url = 'login?un=test&pw=test';
var response = {"token":1}
$httpBackend.expectGET(url)
.respond(200, response);
$http.get(url).success(successCallback);
expect(successCallback).not.toHaveBeenCalled();
$httpBackend.flush();
expect(successCallback.token).toBe(1);
}));
});
}
);
How ever I keep getting TypeError: 'undefined' is not an object (evaluating 'successCallback.token) For reference my LoginFactory looks like this:
var LoginFactory = function ($q, $http) {
return {
getData: function (url) {
var deferred = $q.defer();
$http.get('http://local/'+url)
.then(function () {
deferred.resolve(true);
}, function () {
deferred.resolve(false);
});
return deferred.promise;
}
};
};
LoginFactory.$inject = ['$q', '$http'];
factories.factory('LoginFactory', LoginFactory);
return LoginFactory;
Thanks in advance!
As you mentioned already, successCallback.token is the point where your code is breaking because jasmine.createSpy() returns a function but it has no clue about your token from your mock response served by $httpBackend. From the official docs,
When there is not a function to spy on, jasmine.createSpy can create a “bare” spy.
This spy acts as any other spy – tracking calls, arguments, etc.
But there is no implementation behind it. Spies are JavaScript objects
and can be used as such.
So removing this line expect(successCallback.token).toBe(1); will resolve the error but if you actually want to verify the mock response from $httpBackend, you need to do that in success callback on $http (or) in the promise object's then method like below
$http.get(url).success(function(response){
expect(response.token).toBe(1);
});
Note that here I just modified your code to show the example, ideally you don't code the HTTP call separately in your test case, you invoke the actual function making the HTTP call and verify the expectations.
For more details on $httpBackend, have a look at this link.
I'm trying to write a test for a controller that has $rootScope.$on('accountsSet', function (event).... So in the tests I'm using .broadcast.andCallThrough() which many other questions here in SO suggest while it also worked before for me.
So my controller is pretty simple:
angular.module('controller.sidemenu', [])
.controller('SidemenuCtrl', function($rootScope, $scope, AccountsService) {
$rootScope.$on('accountsSet', function (event) {
$scope.accounts = AccountsService.getAccounts();
$scope.pro = AccountsService.getPro();
});
});
Any the test is simple as well:
describe("Testing the SidemenuCtrl.", function () {
var scope, createController, accountsService;
beforeEach(function(){
angular.mock.module('trevor');
angular.mock.module('templates');
inject(function ($injector, AccountsService) {
scope = $injector.get('$rootScope');
controller = $injector.get('$controller');
accountsService = AccountsService;
createController = function() {
return controller('SidemenuCtrl', {
'$scope' : $injector.get('$rootScope'),
'AccountsService' : accountsService,
});
};
});
});
it("Should load the SidemenuCtrl.", function () {
accountsService.setPro(true);
spyOn(scope, '$broadcast').andCallThrough();
var controller = createController();
scope.$broadcast("accountsSet", true);
expect(scope.pro).toBeTruthy();
});
});
The error I'm getting if for spyOn(scope, '$broadcast').andCallThrough();. Note that scope for this tests is rootScope so that shouldn't be a problem.
So the error that refers to that line:
TypeError: 'undefined' is not a function (evaluating 'spyOn(scope, '$broadcast').andCallThrough()')
at .../tests/controllers/sidemenu.js:30
I'm turning my comment into an answer since it turned out to be the solution:
In jasmine 2.0 the syntax of spies has changed (and many other things, see the beautiful docs here)
the new syntax is
spyOn(foo, 'getBar').and.callThrough();
Compare with the jasmine 1.3 syntax of:
spyOn(foo, 'getBar').andCallThrough();
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.