Archive for July, 2010

HTML fixtures in Jasmine (using jasmine-jquery)

!!! THIS BLOG IS NO LONGER ACTIVE – CHECK OUT MY CURRENT BLOG AT VELESIN.IO !!!

My comparison of JavaScript BDD frameworks resulted in choosing Jasmine to be my JS testing framework of choice. However, I’m doing a lot of jQuery based development, while Jasmine by design tries to stay DOM agnostic. It is not that big problem, as Jasmine is really flexible and open (I’ll show in a moment how to manipulate DOM with jQuery using plain-vanilla Jasmine), but with a couple of tweaks packed into my jasmine-jquery plugin, jQuery users can make their life even easier.

First, let’s start with the simplest approach possible to test jQuery code in your Jasmine specs. The most straightforward way is just to create a “fixture” jQuery element inside your spec, invoke your plugin on it and then set your expectations against the fixture element (to verify if the plugin modified fixture element correctly):

describe('simplest jQuery plugin test', function () {
  it('should use inline fixture', function () {
    var fixture = $('<div>some HTML code here</div>');
    fixture.myTestedJqueryPlugin();
    expect(fixture).toSomething();
  });
});

Of course, for any non trivial plugin you’ll need more than one spec to fully test its behavior, so the above example in reality will look more like this:

describe('more realistic jQuery plugin test', function () {
  var fixture;

  beforeEach(function () {
    fixture = $('<div>some HTML code here</div>');
  });

  it('should use inline fixture', function () {
    fixture.myTestedJqueryPlugin();
    expect(fixture).toSomething();
  });

  it('should use inline fixture again', function () {
    fixture.myTestedJqueryPlugin();
    expect(fixture).toSomethingElse();
  });
});

Above solution is still far from perfect though. One of the problems is that jQuery fixture element is created only in memory and never added to DOM. In effect, many tests (e.g. related to element visibility or dimensions) won’t work correctly. In order to solve this, we need to add our fixture to the DOM and, to avoid test interdependencies, we must clear the DOM between tests.

describe('DOM based jQuery plugin test', function () {
  beforeEach(function () {
    $('#fixture').remove();
    $('body').append('<div id="fixture">...</div>');
  });

  it('should use DOM fixture', function () {
    $('#fixture').myTestedJqueryPlugin();
    expect($('#fixture')).toSomething();
  });

  it('should use DOM fixture again', function () {
    $('#fixture').myTestedJqueryPlugin();
    expect($('#fixture')).toSomethingElse();
  });
});

Now it should work for all kinds of jQuery related tests, and the beforeEach doesn’t look that bad (only 2 lines of code), but unfortunately such a simple one-liner fixtures will rarely suffice for non trivial, real-life plugins. And believe me, inlining complex, multiline HTML strings in your test code is ugly, turning your well factored test code into one big tangled mess. Also, to keep your tests clean, you may want to reuse fixtures not only between tests in one file, but also between different test files. The answer here is loading fixtures from files. We can change our code to something like this:

describe('test with fixture files', function () {
  beforeEach(function () {
    $('#fixture').remove();
    $.ajax({
      async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
      dataType: 'html',
      url: 'my_fixture.html',
      success: function(data) {
        $('body').append($(data));
      }
    });
  });

  it('should use DOM fixture', function () {
    $('#fixture').myTestedJqueryPlugin();
    expect($('#fixture')).toSomething();
  });

  it('should use DOM fixture again', function () {
    $('#fixture').myTestedJqueryPlugin();
    expect($('#fixture')).toSomethingElse();
  });
});

Now your fixtures can be neatly organized into separate files and don’t pollute your test code. An added benefit is that they don’t have to be static – my_fixture.html file can be now e.g. generated via normal Rails action etc. However, beforeEach have grown a little. And this is just a tip of an iceberg. Right now the fixture file is reloaded via AJAX before each of your tests, which results in a major penalty to your test suite’s speed – some kind of fixture caching would be welcome. Also, what if you want to load more than one fixture? You’ll then have to duplicate fixture loading and DOM clean-up code. And what if for some reason you want to load fixture from external file, but don’t want to append it automatically to DOM, but rather assign to a variable inside your test? Again, this can be easily done, but it will add another variation to your beforeEach code. Even worse, you’ll probably use fixtures in more than one test file, so your fixture loading code will end-up copy pasted all over the place. As you can see this can quickly go out of control for any non trivial project.

jasmine-jquery to the rescue! ;)

It extends Jasmine framework with a robust set of methods to handle HTML fixtures. It supports also fixture caching to speed-up your test suite and auto clean-up of DOM between tests. With jasmine-jquery you can write your tests like this:

describe('test with jasmine-jquery', function () {
  it('should load many fixtures into DOM', function () {
    loadFixtures('my_fixture_1.html', 'my_fixture_2.html');
    expect($('#jasmine-fixtures')).toSomething();
  });

  it('should only return fixture', function () {
    var fixture = readFixtures('my_fixture_3.html');
    expect(fixture).toSomethingElse();
  });
});

And that’s all. Yes, there doesn’t have to be beforeEach at all. Fixtures are automatically appended to DOM by loadFixtures method and automatically cleaned-up between tests. Apart from loadFixtures and readFixtures jasmine-jquery provides other useful methods, so don’t forget to check jasmine-jquery GitHub page for the full documentation.

Apart of HTML fixtures support, jasmine-jquery provides also a robust set of Jasmine custom matchers for jQuery – I’ll present them in more detail in next post.

Comments (34)