Dynamic dependencies in Jasmine

Despite having worked with JavaScript on most projects, and also unit tested it in the past, I’ve been a bit thrown by the issues I’ve been experiencing recently. I’ve been working on a fairly large existing JavaScript codebase – with no unit tests – to try and bring it under test. We chose to use Jasmine, a BDD testing framework that superseded JSpec. It’s proven to be easy to get to grips with and very pleasant to use, however, as our unit test suite has grown, I’ve tripped over on the dynamic nature of JavaScript.

Jasmine tests are divided into ‘describe’ blocks, which can be nested. Each describe block can have a ‘beforeEach’, however, any code that runs in the beforeEach – and indeed, in any of the tests – can affect the later ones running in the same suite. In particular: creating stubs.

The code we’re writing tests around has a high number of dependencies within it, which means we’ve needed to create rather a lot of stubs for each test. So what happens if I create a stub of an object for one test, then later try to run a test against the real object? It runs against the stub.

Supposing I had a Bookshelf class that looked like this:

var MyLibrary = {};

MyLibrary.BookShelf = function() {

    var books = [];

    this.addBook = function (isbn) {
        books.push(new MyLibrary.Book(isbn));
    }

    this.findBooksBy = function (author) {
        var i,
            matchingBooks = [];

        for(i=0; i<books.length; i++) {
            var book = books[i];
            if (book.isWrittenBy(author)) {
                matchingBooks.push(books)
            }
        }

        return matchingBooks;
    }

};

And supposing my Book class did something when it initialised itself that I didn’t want it to do within the test:

MyLibrary.Book = function(isbn) {
    var _title;
    var _author;
    var _isbn = isbn;

    this.isWrittenBy = function(author) {
        return author == _author;
    }

    var init = function() {
        $.getJSON("/BookDetails?isbn=" + _isbn, function(data) {
            _author = data.author;
            _title = data.title;
        });
    };

    init();
};

I could stub the Book to avoid the init function from running within my bookshelf tests:


describe("Bookshelf", function() {

    it("should find all the books by a given author", function() {

        MyLibrary.Book = function() {
            this.isWrittenBy = function() {return true;}
        };

        var shelf = new MyLibrary.BookShelf();
        shelf.addBook("1234");

        var books = shelf.findBooksBy("somebody");

        expect(books.length).toBe(1);

    });

});

Then I might want to write a test for the Book class. That’s tricky … but I could override the JQuery function:

describe("Book", function() {

    it("should return false if the author does not match", function() {

        $.getJSON = function(url, callback) {
            callback({ author: "Jo Cranford", title: "Post Post Technical" });
        }

        var myBook = new MyLibrary.Book("1234")

        expect(myBook.isWrittenBy("Not Jo Cranford")).toBe(false);
    });

});

If I now run the Bookshelf test followed by the Book test, the book test will fail, because it’s been redefined in my first test.

This may seem obvious, and in this rather contrived example, very easy to fix – but it’s also very easy to overlook, especially if the stub is coming from another file. With a team of several developers, code with lots of dependencies, and a suite of tests that’s growing fairly quickly, it can cause quite a bit of annoyance!

We’ve solved the problem for now by adapting the build to run each spec file in its own sandbox. Each spec file tests one object, and so far this approach is working. However, it does cause the build to run slower, which is fine right now since Jasmine is so fast and we don’t have that many tests, but it may become more painful in the future.

The longer term solution is of course to avoid writing code, even JavaScript code, with nasty dependencies and use Dependency Injection instead :)

Example code is on github here.