Unit testing ampersand.js with Jasmine

I’ve been wanting to write a post about writing unit tests for an Ampersand - or backbone, or even Angular - project for a while now. For my setup, I’m using Ampersand.js - if you don’t know it yet, check out an introductory post I wrote a while ago - , Jasmine and Grunt. The Jasmine tests run in Phantomjs in this setup.

Test Driven Development

Before diving into Jasmine itself, first a word on TDD. Most of you will certainly have heard this term before, some of you will already use it in everyday life. Wikipedia states:

First the developer writes an (initially failing) automated test case that defines a desired improvement or new function, then produces the minimum amount of code to pass that test, and finally refactors the new code to acceptable standards.

So when building, for example, the login method for a User module, you should start by defining what the method should do. Should it create a new User, do a request to the API, save the access token? TDD forces you to think about how you are going to build something before you actually build it.

When your specs are finished, you should start writing the real code, and one by one, the tests should start passing.

Setting up a basic testing environment

To set up your own testing environment you first need some code to write tests for. If you don’t have an Ampersand project lying around, you can clone this Ampersand boilerplate I have set up. It’s a rather clean slate to start with, including only browserify for packaging and compass for compiling scss. I’ll be using the boilerplate myself for this tutorial.

When you’ve got your project, let’s start by installing the jasmine grunt task:

npm install grunt-contrib-jasmine

Don’t forget to load the task by adding this to your Gruntfile.js:

grunt.loadNpmTasks('grunt-contrib-jasmine');

Next up, we’ll define the task, like this:

jasmine: {
    specs: {
        src: [],
        options: {
              outfile: '_SpecRunner.html',
              specs: 'specs.js',
              keepRunner: true   
        }
      }
};

Running grunt jasmine in the console should now trigger the tests. At the moment, they will not work yet though, because we still need to browserify the code necessary for the tests into specs.js.

Let’s just do that:

browserify: {
    specs: {
        src: ['**/specs/**/spec.*.js'],
        dest: 'specs.js',
        options: {
              transform: ['jadeify', 'babelify'],
          },
    }
}

Now, every javascript file inside a specs folder and starting with spec. will be browserified. You can change the src line to whatever works for you, I like to maintain the same structure throughout all my modules.

Last but not least, we should register the grunt task for browserifying and running the tests:

grunt.registerTask('specs',['browserify:specs','jasmine:specs']);

Now, running grunt specs will run the tests correctly. All we need to do now is write some! ;)

Writing your first test

To start writing your first test, create a specs folder, and inside that folder a new js file, like spec.hello.js.
We’re going to start by defining our specs, like proper TDD requires:

describe('The Hello view', function() {
    it('has a property "name"', function() {
        expect(this.hello.name).toBeDefined();
    });
});

Jasmine syntax is in fact extremely readable, when defining a suite, which is a group of specs, use the describe function. Inside each suite, you can define different specs, using the it function.

The expect method defines an expectation; in this case that a certain variable is not undefined. The toBeDefined function here is called a matcher. Check the Jasmine documentation for a complete list of matchers and other Jasmine specific syntax.

Running this test won’t work however, because this.hello.name is not defined. We still need to initialize a new Object, in this case an ampersand-view, to run our tests on. When writing Jasmine tests, you have access to a couple of methods to build up and break down all elements necessary for testing your scenario. Let’s take a look:

import HelloView from '../Views/Hello';    

describe('The Hello view', function() {

    beforeEach(function() {
        this.hello = new HelloView();
    });

    it('has a property "name"', function() {
        expect(this.hello.name).toBeDefined();
    });
});

Add the beforeEach method to create a new instance of HelloView before each spec (the it function) is run.
In the same way, you can add an afterEach to reset this.hello after each spec:

import HelloView from '../Views/Hello';    

describe('The Hello view', function() {

    beforeEach(function() {
        this.hello = new HelloView();
    });

    it('has a property "name"', function() {
        expect(this.hello.name).toBeDefined();
    });

    afterEach() {
        this.hello = null;
    }
});

Testing Views, Models and Collections

Views

It’s one thing to write specs simple methods, like one that rounds a timestamp to midnight, but testing an Ampersand object is a little more complex. If you’re dealing with a View, you’d want to test for different dimensions; initialization, rendering and maybe some interactions.

If an ampersand-view is initialized, it is given a cid property. This is a concatenation of the type of Ampersand object and a number, like ‘view12’. We can easily test if the object we created in the beforeEach function is indeed a view:

it('is a view', function() {
    expect(this.hello.cid).toBeDefined();
    expect(this.hello.cid).toContain("view");
});

Views have templates most of the time, which should be rendered correctly:

it('renders the right template', function() {
    expect(this.hello.el.innerHTML).toContain('<h1>Hello!</h1>');
});

You should also check if DOM-bindings are correct:

it('renders the name in the template', function() {
    var nameEl = this.hello.query('.name');
    expect(nameEl.innerHTML).toEqual('Jane');
});

To check if certain View methods were called during the test, you can use Jasmine spies; they spy on a method, and record wether or not is was called, how many times, and with what parameters.
Using a spy, you could for example check if the render() method was correctly called:

beforeEach(function() {
    this.hello = new HelloView();
    spyOn(this.hello, 'render');
});

it('renders the view', function() {
    expect(this.hello.render).toHaveBeenCalled();
});

Note: when your View is not very elaborate, you can use one suite to group all specs, but most of the time you’ll want to create different suites for different View methods.

Models and Collections

You should keep the same things in mind while writing specs for Models or Collections. Be sure to check if all necessary props are set on initialization, and that their default values are correct using the different matchers). You can also write your own matchers easily, check out the Jasmine documentation.

Testing async code

Although Jasmine itself runs synchronous, that is not the case for most of the client-side javascript written these days. Luckily the Jasmine API has a function that comes in handy when testing async methods: done.

Using done is a little bit tricky in the beginning, because you really need to understand in what order Jasmine functions are run.

Per describe, you can add a beforeEach, afterEach, beforeAll, afterAll and different its. done can be given as an argument to these function and can be called when the async work is done.

I think an example explains it best. In this example, a new User Model is created and then logged in. The login method is a Promise, and the specs should only be tested when the login promise is resolved. Once that happens, done is called.

describe('Logging a user in', function() {

    beforeEach(function(done) {
        this.user = new User();

        spyOn(this.user, 'save');

        this.user.login({
            email: 'silke@test.com',
            password: '12lkasnklasdlZDSD8999dsadas'
        }).then(function() {
            done();
        });
    });

    it('saves the user', function() {
        expect(this.user.save).toHaveBeenCalled();
    });

    afterEach(function() {
        this.user = null;
    });
});

This means that for every spec (it), first beforeEach is called and put on ‘hold’; only when the login promise returns successfully, the spec itself is run.

Final thoughts

There is a lot more to tell about unit testing, since it’s almost an area of expertise in itself. I’m still learning a lot myself, and with every spec I write I learn more and more. Please feel free to get back to me with questions, ideas or suggestions!

If you haven’t written any tests yet, I suggest you start with it on your next project, because unit tests can prevent a lot of bugs and breaking code - trust me ;)

If you’ve never written unittests before, or are completely new to Jasmine or a similar framework, I suggest you read up with Javascript testing with Jasmine, by Evan Hahn.

The Jasmine documentation is also a very good starting point.

A good read on writing tests for native app frameworks - like Backbone or Ampersand - can be found in Developing backbone.js applications, by Addy Osmani, in the chapter TDD With Backbone.

The Sinon documentation can also come in handy when you want to mock or stub. I didn’t go into this yet, but if you’re interested, I’ll write a post on mocking an stubbing in the future.