Spies:
Jasmine integrates
'spies' that permit many spying, mocking, and faking behaviours A 'spy'
replaces the function it is spying on.
One of the
primary aims of unit testing is to isolate a method or component that you want
to test and see how it behaves under a variety of circumstances. These might
include calls with various arguments - or even none at all, - or whether it
calls other methods as it should. Unfortunately, many methods and/or objects
have dependencies on other methods and/or objects, such as network connections,
data sources, files, and even previously executed methods. This is where mocks
come in.
A mock is a fake
object that poses as the real McCoy in order to satisfy the inherent
dependency(ies) without having to go through the overhead of creating the real
object.
Mocks work by implementing the proxy
pattern. When you create a mock object, it creates a proxy object that takes
the place of the real object.
We can then
define what methods are called and their returned values from within our test
method. Mocks can then be utilized to retrieve run-time statistics on the spied
function such as:
- How many times the spied
function was called.
- What was the value that the
function returned to the caller.
- How many parameters the
function was called with.
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:
//spyOn(object, methodName) where
object.method() is a function
spyOn(obj, 'myMethod')
//jasmine.createSpy(stubName);
var myMockMethod =
jasmine.createSpy('My Method');
Using
the spyOn() Method
As
mentioned above, spyOn() can only be used when the method already exists on the
object. For simple tests, this is your best bet.
Our test
cases all feature the following Person object. It has a couple of attributes, a
getter and setter for the name, and two public methods:
var Person = function() {
//defaults
var _age = 0,
_name = 'John Doe';
this.initialize = function(name, age) {
_name = name || _name;
_age
= age || _age;
};
if (arguments.length)
this.initialize();
//getters
and setters
this.getName = function() { return _name; };
this.setName = function (name)
{ _name = name; };
//public
methods
this.addBirthday = function() { _age++; };
this.toString = function() { return 'My name is " + this.getName() + "
and I am " + _age + " years old.'; };
};
Say that we want to verify that the toString() method was
calling getName(). We would instantiate the Person as usual, but before calling
toString(), we would call spyOn(), passing in the person instance and the name
of the method that we want to spy on ('getName'). We can then call jasmine
matchers to see what happened.
The simplest test is
to check that getName() was in fact called:
describe("Person toString() Test", function() {
it("calls
the getName() function", function() {
var testPerson = new Person();
spyOn(testPerson, "getName");
testPerson.toString();
expect(testPerson.getName).toHaveBeenCalled();
});
});
But that's just the beginning. We can run other tests on our
spied function, such as what arguments it was called with. The
toHaveBeenCalledWith() method accepts a value to be compared against the
method's arguments attribute.
Conversely, we can test that the function was called without
any parameters by calling toHaveBeenCalledWith() without a value:
describe("Person toString() Test", function() {
var testPerson;
beforeEach(function() { testPerson = new Person(); });
afterEach (function() { testPerson = undefined; });
it("calls
the getName() function", function() {
spyOn(testPerson, "getName");
testPerson.toString();
expect(testPerson.getName).toHaveBeenCalled();
});
it("Method
getName() was called with zero arguments", function() {
//
Ensure the spy was called with the correct number of arguments
//
In this case, no arguments
expect(testPerson.getName).toHaveBeenCalledWith();
//
this also works
//
expect(testPerson.getName.mostRecentCall.args.length).toEqual(0);
});
});
Creating
Our Own Spy Method
Sometimes, it may be beneficial to completely replace the
original method with a fake one for testing. Perhaps the original method takes
a long time to execute, or it depends on other objects that aren't available in
the test context.
Jasmine lets us handle this issue by creating a fake method
using jasmine.createSpy(). Here's how to substitute a fake getName() for the
real one:
describe("Person toString() Test with Fake
getName() Method", function() {
it("calls
the fake getName() function", function() {
var testPerson = new Person();
testPerson.getName = jasmine.createSpy("getName spy");
testPerson.toString();
expect(testPerson.getName).toHaveBeenCalled();
});
});
Unlike spyOn(), creating a fake method circumvents the
original method so that it is not called during tests. Thus, the alert in
getName() below will not appear:
var Person = function() {
//...
this.getName = function() {
alert("You
called?"); //won't be called
return _name;
};
//...
};
describe("Person toString() Test with Fake
getName() Method", function() {
it("calls
the fake getName() function", function() {
var testPerson = new Person();
testPerson.getName = jasmine.createSpy("getName() spy");
testPerson.toString();
expect(testPerson.getName).toHaveBeenCalled();
});
});
Modifying the Fake
Method
If your method is
being called by another method, you may want it to return something. You can
tell Jasmine what to return using the andReturn(value) method:
testPerson.getName
= jasmine.createSpy("getName()
spy").andReturn("Bobby");
And finally, here's a way to
substitute an entirely different method body for the original:
// defining a spy on an existing
property: testPerson.getName() calls an anonymous function
testPerson.getName = jasmine.createSpy("getName()
spy").andCallFake(function()
{
console.log("Hello from getName()");
return "Bobby";
});
The above function not only
returns "Bobby" each time, but it also logs a message to the console.
That would be a little harder to do with the original function.
Spy specific matchers:
When working with spies, these matchers are quite handy:
//
assumes obj.method is a function
How to spy on a method?
spyOn(obj,
'method')
Pasitive:
//passes if x is a spy and was called
expect(x).toHaveBeenCalled()
How to verify it was called?
expect(obj.method).toHaveBeenCalled()
//passes if x is a spy and was called with the specified
arguments
expect(x).toHaveBeenCalledWith(arguments)
How to verify it was called with specific arguments?
expect(obj.method).toHaveBeenCalledWith('foo', 'bar')
Negative:
//passes if x is a spy and was not called
expect(x).not.toHaveBeenCalled()
//passes if x is a spy and was not called with the specified
arguments
expect(x).not.toHaveBeenCalledWith(arguments)
Spies can be trained to
respond in a variety of ways when invoked:
//spies on AND calls the original function spied on
spyOn(x, 'method').andCallThrough():
How to have spied method also calls through to the real
function?
spyOn(obj,
'method').andCallThrough()
//returns passed arguments when spy is called
spyOn(x, 'method').andReturn(arguments):
How do I fix the return value of a spy?
spyOn(obj, 'method').andReturn('Pow!')
//throws passed exception when spy is called
spyOn(x, 'method').andThrow(exception):
//calls passed function when spy is called
spyOn(x, 'method').andCallFake(function):
Spies can also be
retrained after first being invoked:
spyOn(x, 'method').andReturn(value1); ... ;
x.method.andReturn(value2):
//changes the value
returned by x.method()
Spies have some useful
properties:
//returns number of times spy
was
called
callCount:
How many times was it called?
obj.method.callCount
//returns argument array from last call to spy.
mostRecentCall.args:
What were the arguments to the last call?
obj.method.mostRecentCall.args
How to reset all the calls made to the spy so far?
obj.method.reset()
How to make a standalone spy function?
var
dummy = jasmine.createSpy('dummy')
$('button#mybutton').click(dummy)
How to get all arguments for all calls that have been
made to the spy?
obj.method.
argsForCall //
this is an array
//argsForCall[i]
returns arguments array for call i to spy.
Spies are automatically removed
after each spec. They may be set in the beforeEach function.



