Non-Destructive Spies in Jasmine

Leave a comment

Over the past few months I’ve been building out our UI test framework tools at AppNexus. This gave me the opportunity to research and play with the latest automated testing tools currently available for JavaScript.

I’ve been extremely impressed with the Jasmine BDD framework which is maintained by Pivotal Labs. We’ve begun incorporating it heavily in our development processes and it is helping our code base to mature.

Why Jasmine?

I love the BDD-style interface, both for writing tests, and the reporting output. Jasmine patterns itself after RSpec. Jasmine is designed to take advantage of JavaScript’s strengths. The assertion API has a fluent interface, and anonymous functions are leveraged throughout. Nested describe blocks make it easy to encapsulate repetitive test set-up actions into limited-scope utility functions, which keeps my tests DRY.

Jasmine is extensible. It’s easy to add additional matchers, and since we utilize jQuery heavily at AppNexus, the jasmine-jquery package is extremely helpful.

Jasmine provides a lightweight spy interface (i.e. mocks, test doubles). This is an essential tool for testing our JavaScript in isolation from its dependencies. These dependencies are usually an AJAX back-end. Using Jasmine’s spies I can fake calls to a non-existent server in a test environment. This reduces the complexity of my test environment, and allows me to run tests faster.

One of the first issues I ran into with Jasmine was the destructive nature of its spy creation.

In Jasmine, you create a spy like so:

1
spyOn($, 'ajax');

In the above example, I’m replacing the $.ajax() method with a spy. The spy will record when $.ajax() is invoked in my application code, and it will record what input was given to $.ajax when it was called. After I’ve executed my application code I can verify that the application code correctly invoked $.ajax()

1
expect($.ajax).toHaveBeenCalledWith(someParams);

In JavaScript, functions can have properties of their own. I can define a function like so:

1
2
3
4
5
6
7
function sayHello() {
    console.log('Hello');
}
sayHello.makeGuestsComfortable = function() {
    sayHello();
    console.log('Please sit down');
}

In this example, the sayHello() function has a method called makeGuestsComfortable().

Here’s a Jasmine spec that spies on sayHello():

1
2
3
4
5
6
7
8
9
describe("Greeter", function() {
   it('says hello', function() {
    spyOn(window, 'sayHello');

        sayHello.makeGuestsComfortable();

    expect(window.sayHello).toHaveBeenCalled();
    });
});

But this would fail with something close to the following error:¬†TypeError: Object function () { … } has no method ‘makeGuestsComfortable’. This is because the spyOn() call destroyed the properties of the sayHello function.

We can avoid this by wrapping our call to spyOn in a function which preserves the properties of the spied function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
describe("Greeter", function() {

  var niceSpy = function(obj, funcName) {
    var original = obj[funcName];
    var spy = spyOn(obj, funcName);

    for (var i in original) {
        if (original.hasOwnProperty(i)) spy[i] = original[i];
    }

    return spy;
   }

   it('says hello', function() {
    niceSpy(window, 'sayHello');

        sayHello.makeGuestsComfortable();

    expect(window.sayHello).toHaveBeenCalled();
   });
});

Now the test will pass because the makeGuestsComfortable() function has been preserved.

Jasmine is a fantastic testing tool, and part of a new breed of JavaScript testing frameworks that allow software developers to keep their JavaScript code tested. I’m really excited to see this project grow and help in the development of quality software.

Note: this post was reposted from Eric’s personal blog.

About Eric

UI Engineer at AppNexus

This entry was posted in Development Process. Bookmark the permalink.

Leave a comment