Spying with Jasmine

I really like using Jasmine to write unit tests in JavaScript. It’s easy to use, and the way the expects are written feels really natural. I’ve made a lot of use of Spies in tests I’ve written recently, and they’re also pretty awesome. I’ve used them in a few different ways:
* As a basic mock object – to verify that a method is being called or not called
* To control the value that’s being returned
* To fire a callback function
* To check the arguments that are being passed to a method are what I expect

Of course you could do all of this by hand crafting mock objects, but using the Jasmine spies usually means there is less code, and the tests become far more readable. The other advantage to using Spies is that Jasmine will remove the spies at the end of the tests, so it avoids the problem we had previously of mocks in one test overwriting real objects that we needed for another test.

Verifying a method is called or not

There are two ways to create a spy in Jasmine:
* spyOn
* jasmine.createSpy

spyOn can only be used when the method already exists on the object, whereas jasmine.createSpy will return a function.

Take this example of an object that would take a JQuery element and toggle the display property:

var Toggleable = function(element) {

    this.toggle = function() {
        if (currentlyVisible()) {
            hide();
        } else {
            show();
        }
    }



    function currentlyVisible() {
        return element.css("display") === "block";
    }

    function show() {
        element.show();
    }

    function hide() {
        element.hide();
    }

}

When testing the toggle function, I could use spyOn with a real JQuery object, or jasmine.createSpy to create a fake one. Below are two different ways to write a test that verifies that the jquery show method is called on the object if it’s currently hidden.

it("can create a method for you", function() {

    var fakeElement = {};
    fakeElement.css = function() {
    };
    fakeElement.show = jasmine.createSpy("Show spy");

    var toggleable = new Toggleable(fakeElement);

    toggleable.toggle();

    expect(fakeElement.show).toHaveBeenCalled();

});

it("can spy on an existing method", function() {

    var fakeElement = $("<div style='display:none'></div>");
    spyOn(fakeElement, 'show');

    var toggleable = new Toggleable(fakeElement);

    toggleable.toggle();

    expect(fakeElement.show).toHaveBeenCalled();

});

I could also use spies to verify that a method hasn’t been called. For example, I could add another expect to the second test above:

it("can tell you when a method has - and hasn't - been called", function() {

    var fakeElement = $("<div style='display:none'></div>");
    spyOn(fakeElement, 'show');
    spyOn(fakeElement, 'hide');

    var toggleable = new Toggleable(fakeElement);

    toggleable.toggle();

    expect(fakeElement.show).toHaveBeenCalled();
    expect(fakeElement.hide).not.toHaveBeenCalled();

});

Using a Spy to control the return value

In the first test above, I had to stub the css function since that was also being called in the toggle() function. I could also use a spy for this – although I don’t care whether or not it’s called, it might make the code slightly more readable. Here’s the same test with the two spies:

it("can create a method for you", function() {
	
    var fakeElement = {};
    fakeElement.css = jasmine.createSpy("CSS spy").andReturn("none");
    fakeElement.show = jasmine.createSpy("Show spy");

    var toggleable = new Toggleable(fakeElement);

    toggleable.toggle();

    expect(fakeElement.show).toHaveBeenCalled();

});

There isn’t a really nice way to return different values depending on the arguments that are passed into a spy, but it is possible using andCallFake(). Here’s the same test (again, sorry, it’s getting a bit boring now, isn’t it?) rewritten with andCallFake:

it("can create a method for you with some logic", function() {
	
    var fakeElement = {};
    fakeElement.css = jasmine.createSpy("CSS spy").andCallFake(function(property) {
    	if (property === "display") {
        	return "none";
        }
    });

    fakeElement.show = jasmine.createSpy("Show spy");

    var toggleable = new Toggleable(fakeElement);

    toggleable.toggle();

    expect(fakeElement.show).toHaveBeenCalled();

});

Firing a Callback

Quite regularly, I want to unit test methods that call out to another method and pass it a callback, for example, if it’s making an ajax request and setting the html of an element based on what’s returned.

var DataContainer = function(element) {

	this.loadData = function() {
		$.ajax({
			url: 'http://postposttechnical.com/',
			context: document.body,
			success: putDataInElement
		});
	}

	function putDataInElement(data) {
		element.html(data);
	}

}

In this case, I could use a spy to call the callback, using andCallFake, then use toHaveBeenCalled to spy on the method in the callback that I’m interested in:

it("can call a callback that's passed", function() {

    var fakeElement = {};
    fakeElement.html = jasmine.createSpy("html for fake element");

    var container = new DataContainer(fakeElement);

    var fakeData = "This will be the new html";
    $.ajax = jasmine.createSpy().andCallFake(function(params) {
        params.success(fakeData);
    });

    container.loadData();

    expect(fakeElement.html).toHaveBeenCalled();
});

The ajax spy will be passed the callback that sets the html, and pass it our fake data. Here’s a pretty contrived example that would make sure we were setting the html of the element, and not the text. It might be more useful if you have if / else branches in the code, to validate that the correct methods are being called.

it("can tell if a method has been called or not", function() {

    var fakeElement = {};
    fakeElement.html = jasmine.createSpy("html for fake element");
    fakeElement.text = jasmine.createSpy("text for fake element");

    var container = new DataContainer(fakeElement);

    var fakeData = "This will be the new html";
    $.ajax = jasmine.createSpy().andCallFake(function(params) {
        params.success(fakeData);
    });

    container.loadData();

    expect(fakeElement.html).toHaveBeenCalled();
    expect(fakeElement.text).not.toHaveBeenCalled();
});

Verifying Arguments

In the example above, I might accidentally forget to pass the data into the element.html() call. We can also test for that using the Jasmine function toHaveBeenCalledWith.

Here’s how that might work in the test above:

it("can tell if a method has been called or not and check the parameters", function() {

    var fakeElement = {};
    fakeElement.html = jasmine.createSpy("html for fake element");
    fakeElement.text = jasmine.createSpy("text for fake element");

    var container = new DataContainer(fakeElement);

    var fakeData = "This will be the new html";
    $.ajax = jasmine.createSpy().andCallFake(function(params) {
    	params.success(fakeData);
    });

    container.loadData();

    expect(fakeElement.html).toHaveBeenCalledWith(fakeData);
});

toHaveBeenCalledWith will return true if there has been at least one call with the matching arguments, even if there is more than one. To use this, you must supply all of the arguments in the right order – which may not always be possible. Consider if I wanted to check that the ajax call was made – the success callback is an internal function that I can’t create here. However, I can use jasmine.Any instead – here’s how it works:

it("can check the arguments passed to a function", function() {

    var fakeElement = {};
    fakeElement.html = jasmine.createSpy("html for fake element");

    var container = new DataContainer(fakeElement);

    var fakeData = "This will be the new html";
    $.ajax = jasmine.createSpy("Ajax Spy").andCallFake(function(params) {
        params.success(fakeData);
    });

    container.loadData();

    expect($.ajax).toHaveBeenCalledWith({
        url : 'http://postposttechnical.com/',
        context : document.body,
        success : jasmine.any(Function)
    });

});

All of the code examples here can be found on Github.