When testing part of an AngularJS Application which uses both $timeout and $timeout.cancel with Jasmine's spyOn method.
describe('<whatever>', function() {
beforeEach(function() {
spyOn(this, '$timeout').andCallThrough();
spyOn(this.$timeout, 'cancel').andCallThrough();
this.createController();
});
it('should <whatever>', function() {
expect(this.$timeout).toHaveBeenCalled();
expect(this.$timeout.cancel).toHaveBeenCalled();
});
});
You should encounter the following error in your application code, which is using what your test injected into it.
TypeError: 'undefined' is not a function (evaluating '$timeout.cancel(timeoutPromise)');
If you were to run console.log(Object.keys(this.$timeout)); in your test suite, you will see the following output;
LOG: ['identity', 'isSpy', 'plan', 'mostRecentCall', 'argsForCall', 'calls', 'andCallThrough', 'andReturn', 'andThrow', 'andCallFake', 'reset', 'wasCalled', 'callCount', 'baseObj', 'methodName', 'originalValue']
$timeout is a function which AngularJS is also decorating—since functions are objects—with a cancel method. Because this isn't that common a thing to do, Jasmine replaces rather than augments $timeout with it's spying implementation - clobbering $timeout.cancel.
A workaround for this is to put the cancel spy back again after $timeout has been overwritten by Jasmine's $timeout spy, as follows;
describe('<whatever>', function() {
beforeEach(function() {
spyOn(this.$timeout, 'cancel').andCallThrough();
var $timeout_cancel = this.$timeout.cancel;
spyOn(this, '$timeout').andCallThrough();
this.$timeout.cancel = $timeout_cancel;
this.createController();
});
it('should <whatever>', function() {
expect(this.$timeout).toHaveBeenCalled();
expect(this.$timeout.cancel).toHaveBeenCalled();
});
});
This worked for me for $interval, so it should work for $timeout. (jasmine 2)
var $intervalSpy = jasmine.createSpy('$interval', $interval).and.callThrough();
Then I can do both:
expect($intervalSpy.cancel).toHaveBeenCalledTimes(1);
expect($intervalSpy).toHaveBeenCalledTimes(1);
Related
How do I mock the timeout call, here?
$scope.submitRequest = function () {
var formData = getData();
$scope.form = JSON.parse(formData);
$timeout(function () {
$('#submitForm').click();
}, 2000);
};
I want to see timeout has been called with the correct function.
I would like an example of the spyon function mocking $timeout.
spyOn(someObject,'$timeout')
First of all, DOM manipulation should only be performed in directives.
Also, it's better to use angular.element(...), than $(...).
Finally, to do this, you can expose your element's click handler to the scope, spy on it, and check if that handler has been called:
$timeout.flush(2000);
$timeout.verifyNoPendingTasks();
expect(scope.myClickHandler).toHaveBeenCalled();
EDIT:
since that's a form and there is no ng-click handler, you can use ng-submit handler, or add a name to your form and do:
$timeout.flush(2000);
$timeout.verifyNoPendingTasks();
expect(scope.formName.$submitted).toBeTruthy();
$timeout can be spied or mocked as shown in this answer:
beforeEach(module('app', ($provide) => {
$provide.decorator('$timeout', ($delegate) => {
var timeoutSpy = jasmine.createSpy().and.returnValue($delegate);
// methods aren't copied automatically to spy
return angular.extend(timeoutSpy, $delegate);
});
}));
There's not much to test here, since $timeout is called with anonymous function. For testability reasons it makes sense to expose it as scope/controller method:
$scope.submitFormHandler = function () {
$('#submitForm').click();
};
...
$timeout($scope.submitFormHandler, 2000);
Then spied $timeout can be tested:
$timeout.and.stub(); // in case we want to test submitFormHandler separately
scope.submitRequest();
expect($timeout).toHaveBeenCalledWith(scope.submitFormHandler, 2000);
And the logic inside $scope.submitFormHandler can be tested in different test.
Another problem here is that jQuery doesn't work well with unit tests and requires to be tested against real DOM (this is one of many reasons why jQuery should be avoided in AngularJS applications when possible). It's possible to spy/mock jQuery API like shown in this answer.
$(...) call can be spied with:
var init = jQuery.prototype.init.bind(jQuery.prototype);
spyOn(jQuery.prototype, 'init').and.callFake(init);
And can be mocked with:
var clickSpy = jasmine.createSpy('click');
spyOn(jQuery.prototype, 'init').and.returnValue({ click: clickSpy });
Notice that it's expected that mocked function will return jQuery object for chaining with click method.
When $(...) is mocked, the test doesn't require #submitForm fixture to be created in DOM, this is the preferred way for isolated unit test.
Create mock for $timeout provider:
var f = () => {}
var myTimeoutProviderMock = () => f;
Use it:
beforeEach(angular.mock.module('myModule', ($provide) => {
$provide.factory('$timeout', myTimeoutProviderMock);
}))
Now you can test:
spyOn(f);
expect(f).toHaveBeenCalled();
P.S. you'd better test result of function in timeout.
Assuming that piece of code is within the controller or being created in the test by $controller, then $timeout can be passed in the construction parameter. So you could just do something like:
var timeoutStub = sinon.stub();
var myController = $controller('controllerName', timeoutStub);
$scope.submitRequest();
expect(timeoutStub).to.have.been.called;
Unit Tesitng $timeout with flush delay
You have to flush the queue of the $timeout service by calling $timeout.flush()
describe('controller: myController', function(){
describe('showAlert', function(){
beforeEach(function(){
// Arrange
vm.alertVisible = false;
// Act
vm.showAlert('test alert message');
});
it('should show the alert', function(){
// Assert
assert.isTrue(vm.alertVisible);
});
it('should hide the alert after 5 seconds', function(){
// Act - flush $timeout queue to fire off deferred function
$timeout.flush();
// Assert
assert.isFalse(vm.alertVisible);
});
})
});
Please checkout this link http://jasonwatmore.com/post/2015/03/06/angularjs-unit-testing-code-that-uses-timeout
I totally agree with Frane Poljak's answer. You should surely follow his way. Second way to do it is by mocking $timeout service like below:
describe('MainController', function() {
var $scope, $timeout;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller, $injector) {
$scope = $rootScope.$new();
$timeout = jasmine.createSpy('$timeout');
$controller('MainController', {
$scope: $scope,
$timeout: $timeout
});
}));
it('should submit request', function() {
$scope.submitRequest();
expect($timeout).toHaveBeenCalled();
});
Here is the plunker having both approaches: http://plnkr.co/edit/s5ls11
Learning jasmine for the first time and I am stuck on this error when trying to test the focus() functionality in an angular service.
Here is the service:
myApp.service('MyService', function($timeout, $window) {
var service = {
focusElem: focusElem
};
return service;
function focusElem(id) {
console.log('id of element is = ', id);
if (id) {
$timeout(function() {
var element = $window.document.getElementById(id);
console.log('element is = ', element);
if (element) {
element.focus();
}
});
}
};
});
Here is my spec file
describe('myApp', function() {
var element, dummyElement;
beforeEach(function() {
// Initialize myApp injector
module('myApp');
// Inject instance of service under test
inject(function($injector) {
MyServiceObj = $injector.get('MyService');
});
element = angular.element('<input id="firstName" name="firstName"/>');
dummyElement = document.createElement('input');
dummyElement.setAttribute('id', 'lastName');
});
it('should have focus if the focus Service is used on an element', function() {
console.info('------------------');
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
expect(dummyElement.focus).toHaveBeenCalled();
});
});
My error:
myApp should have focus if the focus Service is used on an element
Expected spy focus to have been called.
Error: Expected spy focus to have been called.
If you are using ngMock many services are changed so they can be controlled in a synchronous manner within test code to give you more control over the flow.
One of the affected services is $timeout.
The function passed to $timeout inside your service will not execute in your test unless you tell it to.
To tell it to execute use $timeout.flush() like this:
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
$timeout.flush();
expect(dummyElement.focus).toHaveBeenCalled();
Note that you need a reference to the $timeout service:
var element, dummyElement, $timeout;
beforeEach(function() {
module('myApp');
inject(function($injector, _$timeout_) {
MyServiceObj = $injector.get('MyService');
$timeout = _$timeout_;
});
The next problem is due to the following line in your service:
var element = $window.document.getElementById(id);
The elements you create in your test are never attached to the DOM, so the service will not find them.
The easiest solution is to just attach your elements to the DOM. In this case it's important that you remove them manually after the test, since Jasmine uses the same DOM for your entire test suite.
For example:
it('should have focus if the focus Service is used on an element', function() {
var body = angular.element(document.body);
body.append(element);
body.append(dummyElement);
spyOn(element[0], 'focus');
spyOn(dummyElement, 'focus');
MyServiceObj.focusElem(dummyElement.getAttribute('id'));
$timeout.flush();
expect(dummyElement.focus).toHaveBeenCalled();
element.remove();
dummyElement.remove();
});
Demo: http://plnkr.co/edit/F8xqfYYQGa15rwuPPbN2?p=preview
Now, attaching and removing elements to the DOM during unit tests are not always a good thing to do and can get messy.
There are other ways to handle it, for example by spying on getElementById and controlling the return value or by mocking an entire document. I won't go into that here however as I'm sure there are examples of it around here already.
I am trying to perform unit testing with Karma. I have done everything according to the documentation. When I write this part of the test that follows it never calls the last two functions.
it('should create the mock object', function (done) {
service.createObj(mockObj)
.then(test)
.catch(failTest)
.finally(done);
});
var test = function() {
expect(2).toEqual(1);
};
var failTest = function(error) {
expect(2).toEqual(1);
};
Try to inject into your beforeEach function rootScope. For example like this:
var rootScope;
beforeEach(inject(function (_$rootScope_) {
rootScope = _$rootScope_.$new();
//other injections
}));
and next invoke $digest() after your service method:
it('should create the mock object', function (done) {
service.createObj(mockObj)
.then(test)
.catch(failTest)
.finally(done);
rootScope.$digest();
});
Install angular-mocks module.
Inject module with module in beforeEach.
Inject your service with inject function in beforeEach.
Use $httpBackend to simulate your server.
Do, not forget, to make it, sync. with $http.flush().
I'm trying to spy on methods defined in a controller, but no matter what I do I see test failures with the message:
Error: Expected a spy, but got Function.
I'm using Karma, Jasmine and Sinon alongside Angular. I'm pretty sure things are set up correctly because tests that just read properties from the $scope pass.
For example, I have this very simple app module:
angular.module('app', []);
And this very simple controller:
angular.module('app').controller('myController', ['$scope', function($scope) {
$scope.test = '';
$scope.setTest = function (newString) {
$scope.test = newString || 'default';
}
$scope.updateTest = function (newString) {
$scope.setTest(newString);
};
}]);
My spec file is as follows:
describe('myController', function () {
'use strict';
beforeEach(module('app'));
var $scope, sandbox;
beforeEach(inject(function ($controller) {
$scope = {};
$controller('myController', { $scope: $scope });
sandbox = sinon.sandbox.create();
}));
afterEach(function () {
sandbox.restore();
});
describe('#updateTest()', function () {
beforeEach(function () {
sandbox.spy($scope, 'setTest');
});
it('updates the test property with a default value', function () {
$scope.updateTest();
expect($scope.test).toEqual('default');
});
it('calls the setTest method', function () {
$scope.updateTest();
expect($scope.setTest).toHaveBeenCalled();
});
});
});
The first test (where it's just checking the test property gets updated) passes.
The second test, where I just want to spy on the setTest() method, fails with the error message above.
If I log out the $scope in the beforeEach I can see the setTest method and there are no script errors.
What am I missing?
I guess it's happening because you are mixing Jasmine and Sinon, I do no think that Sinon spy sandbox.spy() gonna work with Jasmine matcher expect().toHaveBeenCalled(). You should choose which one to use:
Use Sinon spies and convert the result to primitive to pass it to Jasmine:
sandbox.spy($scope, 'setTest');
expect($scope.setTest.called).toBeTruthy();
But this approach will give you less verbose output: Expected true to be false, instead of usual Expected spy to have been called.
Use Jasmine spies:
spyOn($scope, 'setTest');
expect($scope.setTest).toHaveBeenCalled();
Also you can take a look at the tool jasmine-sinon, which adds extra Jasmine matchers and allows to use Sinon spies with Jasmine spy matchers. As a result you should be able to use like in your sample:
sandbox.spy($scope, 'setTest');
expect($scope.setTest).toHaveBeenCalled();
I'm trying to test my AngularJS controller with Jasmine, using Karma. But a $timeout which works well in real-life, crashes my tests.
Controller:
var Ctrl = function($scope, $timeout) {
$scope.doStuff = function() {
$timeout(function() {
$scope.stuffDone = true;
}, 250);
};
};
Jasmine it block (where $scope and controller have been properly initialized):
it('should do stuff', function() {
runs(function() {
$scope.doStuff();
});
waitsFor(function() {
return $scope.stuffDone;
}, 'Stuff should be done', 750);
runs(function() {
expect($scope.stuffDone).toBeTruthy();
});
});
When I run my app in browser, $timeout function will be executed and $scope.stuffDone will be true. But in my tests, $timeout does nothing, the function is never executed and Jasmine reports error after timing out 750 ms. What could possibly be wrong here?
According to the Angular JS documentation for $timeout, you can use $timeout.flush() to synchronously flush the queue of deferred functions.
Try updating your test to this:
it('should do stuff', function() {
expect($scope.stuffDone).toBeFalsy();
$scope.doStuff();
expect($scope.stuffDone).toBeFalsy();
$timeout.flush();
expect($scope.stuffDone).toBeTruthy();
});
Here is a plunker showing both your original test failing and the new test passing.
As noted in one of the comments, Jasmine setTimeout mock is not being used because angular's JS mock $timeout service is used instead. Personally, I'd rather use Jasmine's because its mocking method lets me test the length of the timeout. You can effectively circumvent it with a simple provider in your unit test:
module(function($provide) {
$provide.constant('$timeout', setTimeout);
});
Note: if you go this route, be sure to call $scope.apply() after jasmine.Clock.tick.
As $timeout is just a wrapper for window.setTimeout, you can use jasmines Clock.useMock() which mocks the window.setTimeout
beforeEach(function() {
jasmine.Clock.useMock();
});
it('should do stuff', function() {
$scope.doStuff();
jasmine.Clock.tick(251);
expect($scope.stuffDone).toBeTruthy();
});