Pragmatic JavaScript Testing with Jasmine

Posted on by in Development, Process

As more and more parts of our applications are written in JavaScript, its important to have them covered with automated tests. Fortunately, there are numerous JavaScript testing tools available. As a BDD fan, the RSpec inspired Jasmine is currently my go-to.

The Basics

For developers coming from RSpec, Jasmine will feel very familiar.

Here’s a simple spec that demonstrates most of the basics:

describe('User', function () {
  beforeEach(function () {
    this.user = new User();
  });

  describe('#age', function () {
    beforeEach(function () {
      this.age = this.user.age;
    });

    it('defaults to 21', function () {
      expect(this.age).toEqual(21);
    });
  });

  describe('#save', function () {
    beforeEach(function () {
      this.age = 30;
      this.user.save({ age: this.age });
    });

    xit('updates itself', function () {
      expect(this.user.age).toEqual(this.age);
    });
  });
});

Use #beforeEach for setup, #describe to group related specs and #it for your examples. The above two examples use the #toEqual matcher. Jasmine comes with basic matchers e.g. #toBe, #toMatch, #toBeNull; check the docs for a complete list. Nested #describe’s are legal but unlike RSpec there’s no #context method.

The last example uses #xit to mark the example as pending. A corresponding #xdescribe also exists to mark a group of specs as pending. These can be useful as a checklist of sorts in a test first workflow.

The abundance of anonymous functions in Jasmine specs can seem a bit strange and verbose but if you’ve done a fair share of JavaScript you’ll quickly get used to it. For those who haven’t just bear with it for now, later I’ll discuss a way to improve the syntax.

Specifying jQuery

With the basics out of the way let’s look at specifying something you’re likely to encounter in every app: jQuery. For jQuery we’ll use the excellent jasmine-jquery library.

jasmine-jquery gives us HTML fixtures and jQuery specific matchers.

Here’s a sample:

describe('thermostat', function () {
  describe('#lossLangage', function () {
    describe('when selecting an inefficient heating temperature', function () {
      loadFixtures('thermostats/show.html');

      beforeEach(function () {
        thermostat.lossLanguage();

        $('#temperature')
          .val('71')
          .change();
      });

      it('warns how much money you could be saving', function () {
        expect($('#warning')).toBeVisible();
      });

      it('includes the inefficient temperature in the warning', function() {
        expect($('#warning')).toHaveText(/71/);
      });
    });
  });
});

This spec is for a #change event handler for a <select> element. #loadFixtures is provided by jasmine-jquery and allows you to use external HTML files for test data.

Here’s the fixture file used above:

thermostats/show.html

<select id="temperature">
  <option>70</option>
  <option>71</option>
  <option>72</option>
  <option>73</option>
  <option>74</option>
  <option>75</option>
</select>

<div id="warning" style="display:none">
</div>

We used two jQuery specific matchers in our examples: #toBeVisible and #toHaveText. These and a lot more are available; be sure to check out the README for the complete list.

If external fixtures files are too heavyweight for your needs jasmine-jquery also provides inline fixtures via #setFixtures.

describe('NewTaskView', function () {
  describe('#render', function () {
    beforeEach(function () {
      setFixtures('<div id="taskForm" />');
      this.newTaskView = new NewTaskView({ model: new Task });

      this.newTaskView.render();
    });

    it('displays a form to enter a new task', function () {
      expect($(this.newTaskView.el)).toContain('form');
    });
  });
});

In this spec our fixture was just a simple <div> element so we decided to define it in the spec. With everything defined in one place it makes the spec easier to understand and follow.

Specifying Ajax

Of course no app is complete without some Ajax. For specifying Ajax we’ll use the excellent jasmine-ajax library. jasmine-ajax monkeypatches jQuery to use a fake XMLHttpRequest object. This fake will store all Ajax requests, allowing you to inspect and even respond to them.

Here’s a example:

describe('EditTaskView', function () {
  describe('#update', function () {
    beforeEach(function () {
      this.task = new Task({ id: 1 });
      this.editTaskView = new EditTaskView({ model: this.task });
      this.editTaskView.render();

      $('form', this.editTaskView.el)
        .find('input[name=description]')
          .val('description')
          .end()
        .submit();
    });

    it('updates its task', function () {
      var request = mostRecentAjaxRequest();
      expect(request.method).toEqual('PUT');
      expect(request.url).toEqual('/tasks/' + this.task.get('id'));
      var params = JSON.parse(request.params);
      expect(params.description).toEqual('description');
    });
  });
});

This example uses jasmine-ajax’s #mostRecentAjaxRequest method to get the last Ajax request. We then specify its method, url and parameters.

Specifying the request is great but only half the battle. We also want to specify our response handlers. Let’s extend the above example with a spec for a failing update:

describe('EditTaskView', function () {
  describe('#update', function () {
    ... // existing successful update spec

    describe('given an invalid task', function () {
      beforeEach(function () {
        this.task = new Task({ id: 1 });
        this.editTaskView = new EditTaskView({ model: this.task });
        this.editTaskView.render();

        $('form', this.editTaskView.el)
          .find('input[name=description]')
            .val('')
            .end()
          .submit();
      });

      it('displays any validation errors', function () {
        var body = {
          description: ["can't be blank"]
        };
        var response = {
          status: 422,
          responseText: JSON.stringify(body)
        };
        var request = mostRecentAjaxRequest();
        request.response(response);
        expect($('#errors', this.editTaskView.el)).toHaveText("Description can't be blank");
      });
    });
  });
});

Our failure specification uses the jasmine-ajax request object’s #response method to simulate a non-success response from the server.

Running Your Specs Headlessly

The Jasmine gem comes with two Rake tasks that you can use to run your specs. rake jasmine will start a Ruby web server to allow you to run your specs from a browser. For those of us who live on the command line and can in no way fathom running tests from a browser there is rake jasmine:ci. Unfortunately this will use Selenium to run your tests in Firefox.

Instead I like to use the jasmine-headless-webkit gem to run specs headlessly with WebKit. jasmine-headless-webkit includes a command line app that can be used to run every spec at once or a single spec at a time.

# run all specs (will try to locate your jasmine.yml config to determine what specs to run)
$ jasmine-headless-webkit

# run an individual spec
$ jasmine-headless-webkit spec/javascripts/models/task_spec.js

jasmine-headless-webkit also supports specs written in CoffeeScript.

Cleaner Specs with CoffeeScript

CoffeeScript is a fantastic language that compiles into JavaScript. Syntactically it really cleans up JavaScript and feels a like a mixture of Ruby and Python.

Here’s a previous spec converted to CoffeeScript:

describe 'NewTaskView', ->
  describe '#render', ->
    beforeEach ->
      setFixtures '<div id="taskForm" />'
      @newTaskView = new NewTaskView model: new Task

      @newTaskView.render()

    it 'displays a form to enter a task', ->
      expect($(@newTaskView.el)).toContain('form')

Basically CoffeeScript is JavaScript with no semi-colons, curly braces, 1/2 the parentheses, a simpler function syntax, Ruby style instance variables, Python style formatting and a whole lot of functional goodness. If you haven’t checked it out yet I suggest you do. Although once you do, you won’t be going back to plain old JavaScript anytime soon.

Start Testing Today

Jasmine is a mature, proven JavaScript testing tool. Coupled with a great community turning out testing tools everyday, there is no longer any excuse to be writing untested JavaScript. So quit putting it off and give it a try today.