Spies: spyOn(), and.callThrough(), and.returnValue(), and.callFake()
Test doubles like mocks, spies, and stubs are integral part of unit testing. In Jasmine, there are few such functions which can stub any function and track calls to it and all its arguments. They are known as spies.
Consider the below constructor function
function Season() {
this.season = 'Spring';
this.nextSeason = function() {
this.season = 'Summer';
return this.season;
},
this.getNextSeason = function() {
return this.nextSeason();
}
};
Inside the following spec, we first create an instance of
Season() and invoke the
getNextSeason() method in the next line.
Season Bars
by NASA. Public Domain
On call of the getNextSeason() method, the
nextSeason() method also gets called, and
hence the value of season gets assigned to
"Summer" and the spec passes.
describe('spyOn() Demo. Season', function() {
it('should be Summer', function() {
var s = new Season();
s.getNextSeason();
expect(s.season).toEqual('Summer');
});
});
The above spy icon is by Freepik from
https://www.flaticon.com
But in unit testing, while testing a particular method/function we may
not always want to call through all the other
dependent methods/functions (it makes sense when you want to
avoid actual AJAX calls which demand some delay time for the
response). Take for example the s object
created in the above spec. We may just want to test its
getNextSeason() method without actually
calling the nextSeason() method. Jasmine
provides the spyOn() function for such
purposes. spyOn() takes two parameters:
the first parameter is the name of the object and the second parameter
is the name of the method to be spied upon. It replaces the spied
method with a stub, and does not actually execute the real method. The
spyOn() function can however be called
only on existing methods.
In the spec below, we spy on the
nextSeason() method just before the
getNextSeason() call
describe('spyOn() Demo. Season', function() {
it('should be Summer', function() {
var s = new Season();
spyOn(s, 'nextSeason');
s.getNextSeason();
expect(s.season).toEqual('Summer');
});
});
Now since the nextSeason() method is spied
on, calling the getNextSeason() method
does not call the actual
nextSeason() method as in the first case,
and hence the value of season is not
changed. The spec fails.
and.callThrough()
To make the spy call the actual method, we chain
and.callThrough() to the
spyOn() function as illustrated in the
below spec
describe('spyOn() Demo. Season', function() {
it('should be Summer', function() {
var s = new Season();
spyOn(s, 'nextSeason').and.callThrough();
s.getNextSeason();
expect(s.season).toEqual('Summer');
});
});
and the season property is changed to
"Summer". The spec passes.
In Sinon, a spy
calls through the method it is spying on. So,
sinon.spy(s,'nextSeason'); in Sinon is
equivalent to
spyOn(s,'nextSeason').and.callThrough();
in Jasmine.
and.returnValue()
A spy can be made to return a preset/fixed value (without the need
for calling the actual methods using
and.callThrough()). This can be achieved
by chaining the spyOn() function with
and.returnValue().
describe('spyOn() Demo. Season', function() {
it('should be Autumn', function() {
var s = new Season();
spyOn(s, 'nextSeason').and.returnValue('Autumn');
s.getNextSeason();
expect(s.nextSeason()).toEqual('Autumn');
});
});
In the above spec, if the
nextSeason() method is not spied upon,
the actual value passed into
expect() would have been just "Summer".
And the spec would have failed.
and.callFake()
A spy can do more than just return a fixed value, it can also
replace an entire spied function using
and.callFake()
describe('spyOn() Demo. Season', function() {
it('should not be Summer', function() {
var s = new Season();
spyOn(s, 'nextSeason').and.callFake(function(){
console.log('in the future');
return 'Winter';
});
s.getNextSeason();
expect(s.nextSeason()).not.toEqual('Summer');
});
});
Arguments can also be passed into
and.callFake()
describe('spyOn() Demo. Season', function() {
it('should be Winter in Narnia', function() {
var s = new Season();
spyOn(s, 'nextSeason').and.callFake(function(place){
return 'Winter in ' + place;
});
s.getNextSeason();
expect(s.nextSeason('Narnia')).toEqual('Winter in Narnia');
});
});
Notes
-
The
spyOn()function in Jasmine is also similar to thestub()function in jstest, another JavaScript test framework.