Menu

Call Us0333 0146 683
TECH BLOG

JavaScript is difficult to test?

3-minute read

Christopher Chamberlain

Christopher Chamberlain

23 November 2012

Well that is what I thought, until someone introduced me to Jasmine, no not Aladdin's girlfriend, the Jasmine Testing Framework.

Jasmine

Intro

Firstly, I'm no JavaScript guru, my knowledge level is VERY basic. I do work with JavaScript and jQuery, but previously have not really tried to write tests for it. Instead I have usually copied code from an example I found online; poked at it until it did what I wanted and then never looked at it again. But not this time.

Our company is relatively new to agile processes (2 years'ish) and we are doing great with BDD on the Ruby side of things, but we currently don't have any tests for our JavaScript. So as some JavaScript work needed doing we decided to try to write some tests using jasmine.

In this post I won't cover setting up jasmine, there are a number of tutorials around that do this. I used this and this which showed me how to set up a standalone version which comes with a few tests. I won't cover how jQuery's autocomplete works as this is covered on their site.

Instead I want to talk about what I needed to test and how I had to change our code to make it testable.

The Problem

We have a trade selector that uses jQuery's autocomplete. This takes an array of objects to supply the data for the autocomplete dropdown as well as a parameter used when we redirect after a trade is selected.

var trades = [
    {trade:'Accident investigator', categorycode:'1'},
    {trade:'Accountant',            categorycode:'2'},
    {trade:'Baker',                 categorycode:'3'}
   ]

$("#trade_selector").autocomplete(trades, {
    matchContains: "trade",
    formatItem: function(item) {
        return item.trade;
    }
 }).result(function(event, item) {
    location.href = 'http://some.site.for.business/1.htm?category=' + item.categorycode;
  });

I was given a task of updating this so that the redirect was conditional based on the trade selected. e.g. the Accountant should still redirect to some.site.for.business, but a Baker should redirect to some.site.for.shop. But I wanted to do this test driven.

My first problem was I had no way to cleanly hook into the code and with my lack of understanding of how JavaScript events and inline functions worked the task seemed immense. So I started chipping.

My thought was that I don't need to test 'autocomplete' as that is 'hopefully' tested by the makers of jQuery. I also didn't need to test that the correct item was being passed to the 'result' function as that was working already. What I really wanted to test was the function that was being called from the 'results' function. So my first change wasn't exactly TDD but it opened a crack to let me in:

var redirectOnTrade(event, item){
    location.href = 'http://some.site.for.business/1.htm?category=' + item.categorycode;
}

$("#trade_selector").autocomplete(trades, {
    matchContains: "trade",

    formatItem: function(item) {
        return item.trade;
    }
}).result(redirectOnTrade);

This allowed me to write my first test in my AutocompleteSpec.js file:

describe('should redirect on selecting a trade', function() {
    var businessTradeItem;

    beforeEach(function (){
        businessTradeItem = {trade: 'Foo', categorycode: 1 };
    });

describe('redirecting to business', function() {

    it('a business trade should redirect correctly to business', function() {
        redirectOnTrade('', businessTradeItem);
        expect(location.href).toHaveBeenCalledWith('http://some.site.for.business/1.htm?category=1');
    });
});

Great I had a test and it passed. Right? Wrong! As I was trying to make expectations on location.href (not a function) Jasmine was not able to check expectations and each time I called the test my Jasmine SpecRunner.html would change location to my redirected page. Doh! So my second change was to add another function:

var changeLocation(url){
    location.href = url
}

var redirectOnTrade(event, item){
    changeLocation('http://some.site.for.business/1.htm?category=' + item.categorycode);
}

$("#trade_selector").autocomplete(trades, {
    matchContains: "trade",

    formatItem: function(item) {
        return item.trade;
    }
}).result(redirectOnTrade);

Again, it does mean that all I'm doing is wrapping up functionality into discreet functions but it did allow me to change my tests like so:

describe('should redirect on selecting a trade', function() {
    var businessTradeItem;

    beforeEach(function (){
        businessTradeItem = {trade: 'Foo', categorycode: 1 };
        spyOn(window, 'changeLocation');
    });

    describe('redirecting to business', function() {
        it('a business trade should redirect correctly to business', function() {
            redirectOnTrade('', businessTradeItem);
            expect(window.changeLocation).toHaveBeenCalledWith('http://some.site.for.business/1.htm?category=1');
        });
    });
});

Now I had a valid test and could get on with writing some failing tests. So here is my first:

describe('should redirect on selecting a trade', function() {
    var businessTradeItem;

    beforeEach(function (){
        businessTradeItem = {trade: 'Foo', categorycode: 1, site: 'business' };
        shopTradeItem =     {trade: 'Boo', categorycode: 2, site: 'shop' };

        spyOn(window, 'changeLocation');
    });

    describe('redirecting to business', function() {
        it('a business trade should redirect correctly to business', function() {
            redirectOnTrade('', businessTradeItem);
            expect(window.changeLocation).toHaveBeenCalledWith('http://some.site.for.business/1.htm?category=1');
        });

        it('a shop trade should redirect correctly to shop', function() {
            redirectOnTrade('', shopTradeItem);
            expect(window.changeLocation).toHaveBeenCalledWith('http://some.site.for.shop/1.htm?category=2');
        });
    });
});

So now I could finally get on and implement my new redirect code:

var trades = [
    {trade:'Accident investigator', categorycode:'1', site: 'business' },
    {trade:'Accountant',            categorycode:'2', site: 'business' },
    {trade:'Baker',                 categorycode:'3', site: 'shop' }
]

var changeLocation(url){
    location.href = url
}

var redirectOnTrade(event, item){

if( item.site === 'business' ){
changeLocation('<http://some.site.for.business/1.htm?category=>' + item.categorycode);
} else {
changeLocation('<http://some.site.for.shop/1.htm?category=>' + item.categorycode);
}
}

$("#trade_selector").autocomplete(trades, {
    matchContains: "trade",

    formatItem: function(item) {
        return item.trade;
    }
}).result(redirectOnTrade);

And both my tests pass.

Now I am only showing you a part of the real code and a fraction of the full trade list and there was still a whole load of refactoring to be done but by the time I had finished I had over 800 specs surrounding this code, but I think this illustrates the point.

Jasmine made writing the tests easy and very RSpec like, but your code (even JavaScript) needs to be written for testability. Wrapping up your JavaScript into small discreet functions instead of a large inline set of functions, makes it a lot easier to test your code in isolation.

I used to think JavaScript is difficult to test? Not any more :)

Ready to start your career at Simply Business?

Want to know more about what it's like to work in tech at Simply Business? Read about our approach to tech, then check out our current vacancies.

Find out more

Find this article useful? Spread the word.

Share
Tweet
Post

Keep up to date with Simply Business. Subscribe to our monthly newsletter and follow us on social media.

Subscribe to our newsletter

© Copyright 2019 Simply Business. All Rights Reserved. Simply Business is a trading name of Xbridge Limited which is authorised and regulated by the Financial Conduct Authority (Financial Services Registration No: 313348). Xbridge Limited (No: 3967717) has its registered office at 6th Floor, 99 Gresham Street, London, EC2V 7NG.