Spying not working with Angular, Jasmine and Sinon - angularjs

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

Related

How does the createSpy work in Angular + Jasmine?

I made a simple demo of a factory and I am trying to test this using jasmine. I am able to run the test but I am using the spyOn method. I would rather use jasmine.createSpy or jasmine.createSpyObj to do the same test. Could someone help me to refactor my code so that uses these methods instead in my example?
http://plnkr.co/edit/zdfYdtWbnQz22nEbl6V8?p=preview
describe('value check',function(){
var $scope,
ctrl,
fac;
beforeEach(function(){
module('app');
});
beforeEach(inject(function($rootScope,$controller,appfactory) {
$scope = $rootScope.$new();
ctrl = $controller('cntrl', {$scope: $scope});
fac=appfactory;
spyOn(fac, 'setValue');
fac.setValue('test abc');
}));
it('test true value',function(){
expect(true).toBeTruthy()
})
it('check message value',function(){
expect($scope.message).toEqual(fac.getValue())
})
it("tracks that the spy was called", function() {
expect(fac.setValue).toHaveBeenCalled();
});
it("tracks all the arguments of its calls", function() {
expect(fac.setValue).toHaveBeenCalledWith('test abc');
});
})
update
angular.module('app',[]).factory('appfactory',function(){
var data;
var obj={};
obj.getValue=getValue;
obj.setValue=setValue;
return obj;
function getValue(){
return data;
}
function setValue(datavalue){
data=datavalue;
}
}).controller('cntrl',function($scope,appfactory){
appfactory.setValue('test abc');
$scope.message=appfactory.getValue()
})
I have changed your plunkr:
spy = jasmine.createSpy('spy');
fac.setValue = spy;
Edit
In Jasmine, mocks are referred to as spies. There are two ways to
create a spy in Jasmine: spyOn() can only be used when the method
already exists on the object, whereas jasmine.createSpy() will return
a brand new function.
Found the information here. The link has a lot more information about creating spies.
As said in the comments, you have absolutely no need for spies to test such a service. If you had to write the documentation for your service: you would say:
setValue() allows storing a value. This value can then be retrieved by calling getValue().
And that's what you should test:
describe('appfactory service',function(){
var appfactory;
beforeEach(module('app'));
beforeEach(inject(function(_appfactory_) {
appfactory = _appfactory_;
}));
it('should store a value and give it back',function() {
var value = 'foo';
appfactory.setValue(value);
expect(appfactory.getValue()).toBe(value);
});
});
Also, your service is not a factory. A factory is an object that is used to create things. Your service doesn't create anything. It is registered in the angular module using a factory function. But the service itself is not a factory.

AngularJS, Mocha, Chai: testing service with promises

I've found several posts that shows this code as a way to do async unit testing:
The service:
angular.module('miservices', [])
.service('myAppServices', ['$http', 'httpcalls', function($http, httpcalls) {
this.getAccountType = function(){
return httpcalls.doGet('http://localhost:3010/...').then(function(data){
return data;
}, function(error){
...
});
};
...
The test:
describe('testing myAppServices', function(){
beforeEach(module('smsApp'));
it('should handle names correctly', inject(function(myAppServices){
myAppServices.getAccountType()
.then(function(data) {
expect(data).equal({...});
});
...
We're using AngularJS, Mocha, Chai and we have Sinon installed.
The test never gets to the .then part, but why?
Thanks!
If you are testing your service I would recommend to mock your "httpcalls" service (as that is outside scope in this test).
To mock it you can have several ways, one approach would be to have a mocks module that you use only with your unit tests.
angular.module('miservices.mocks', [])
.service('httpcalls', ['$q', function($q) {
this.returnGet = '';
this.doGet = function(url) {
return $q.when(this.returnGet);
};
};
And your unit test then would be something like:
describe('testing myAppServices', function(){
beforeEach(function() {
module('smsApp');
module('miservices.mocks');
});
it('should handle names correctly', inject(function(myAppServices, httpcalls){
httpcalls.returnGet = 'return data';
myAppServices.getAccountType()
.then(function(data) {
expect(data).equal('return data');
});
...
Because we are inserting the mocks module after application module, httpcalls service gets overwritten by its mock version and allows us to test properly myAppServices without further dependencies.

Unit-Testing with Karma not calling functions

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

AngularJS: spyOn both $timeout and $timeout.cancel

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

AngularJS $timeout function not executing in my Jasmine specs

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

Resources