Alexander Beletsky's development blog

My profession is engineering

Baby steps to Backbone.js: Unit testing of models

Unit testing is important part of development process. If you care about the application state and code quality, sooner or later you start to automate the tests. My experience shows, sooner is better option. There are several approaches to unit testing, `test-first` and `test-after`. There are many holy wars appears on that topic. I would say, both options works - but `test-first` or test driven development, works better for me.

By the end of the day, it’s only important that tests exists and helping to catch regression bugs. Still, developing by `test-first` helps to see the problem before ahead and in general provides better code quality (that would conclude the holy war).

Today we going to write some tests, that would cover our existing model class Feedback.js. Since the code is already written, we will go `test-after` approach. Fortunately, the code is quite simple, so it would not make a problem to unit test that. But before, let’s setup our testing infrastructure.

Folders and files

We’ll have a new folder called `spec`. Inside the spec folder, I’ll have `models` and `views` folders that would contain tests for models and views.




Tests runner

I’ll be using Jasmine test framework. It’s very easy to setup it, what we need is jasmine.js and jasmine.css to be placed on proper folders and setup a test page. Test page is a simple html file, which will be entry point for our testing. If you download jasmine standalone bundle you will see SpecRunner.html inside. It could be easily tailored for custom needs.

In the head part of tests.html we need to reference all required .css and .js files.

<title>Feedback form specs</title>

<link rel="stylesheet" type="text/css" href="content/jasmine.css">

<script type="text/javascript" src="/scripts/libs/jquery-1.7.2.js"></script>
<script type="text/javascript" src="/scripts/libs/underscore.js"></script>
<script type="text/javascript" src="/scripts/libs/backbone.js"></script>
<script type="text/javascript" src="/scripts/libs/jasmine.js"></script>
<script type="text/javascript" src="/scripts/libs/jasmine-html.js"></script>
<script type="text/javascript" src="/scripts/libs/mock-ajax.js"></script>

<!-- Sources -->
<script type="text/javascript" src="/scripts/src/models/Feedback.js"></script>

<!-- Specs -->
<script type="text/javascript" src="/scripts/spec/models/Feedback.spec.js"></script>
    

Jasmine tests in essence

Testing with Jasmine is fun and easy. Jasmine is BDD-style framework, so if you practiced TDD with another frameworks, the style might confuse initially. Let’s review the Jasmine test skeleton.

describe('Jasmine spec', function () {
    var value;

    beforeEach(function () {
        value = 1;
    });

    it ('should fail', function () {
        expect(value).toBe(0);
    });

    describe('when value is changed', function () {
        beforeEach(function () {
            value = 0;
        });

        it ('should pass', function () {
            expect(value).toBe(0);
        })
    });
});

In this example, value is our SUT (System under test). beforeEach() function is `context-establish` function, where SUT is initialized (in TDD it’s both arrange/act). it function is assert part. Here, we set our expectations about which state should SUT be in to. Notice, that beforeEach are nested into describe, so you tweek SUT depending on case.

Writing some tests

The only one functionality that Feedback.js model contains currently is validation. Let’s test that.

describe('Feedback.js spec', function () {
    var model;

    beforeEach(function () {
        model = new Feedback();
    });

    describe('when model is validating', function () {
        var errors;
    });
});

This is something to start with. It does not do any asserts, so now we’ll add some real cases. First case, is than both `email` and `feedback` attributes are absent.

describe('when email and feedback fields are absent', function () {
    beforeEach(function () {
        errors = model.validate({});
    });

    it ('should have 2 errors', function () {
        expect(errors.length).toBe(2);
    });

    it ('should have email fields as invalid', function () {
        expect(errors[0].name).toBe('email');
    });

    it ('should have feedback field as invalid', function () {
        expect(errors[1].name).toBe('feedback');
    });
});

It’s is possible that user put email, but forgot about feedback.

describe('when email is set, but feedback is absent', function () {
    beforeEach(function () {
        errors = model.validate({ email: 'a@a.com'});
    });

    it ('should have 1 error', function () {
        expect(errors.length).toBe(1);
    });

    it ('should have feedback field as invalid', function () {
        expect(errors[0].name).toBe('feedback');
    });

    it ('should have error message', function () {
        expect(errors[0].message).toBeDefined();
    });
});

Moving on, user might put feedback but forgot about email.

describe('when feedback is set, but email is absent', function () {
    beforeEach(function () {
        errors = model.validate({ feedback: 'TDD is awesome'});
    });

    it ('should have 1 error', function () {
        expect(errors.length).toBe(1);
    });

    it ('should have email field as invalid', function () {
        expect(errors[0].name).toBe('email');
    });

    it ('should have error message', function () {
        expect(errors[0].message).toBeDefined();
    });
});

Tests report

If you now try to run test.html in browser, you will something like that.




Conclusions

Testing of Backbone.Model’s is pretty simple thing. It’s nothing more than testing some business logic, that might reside inside. Testing of views is a bit trickier thing, but we will see how to do that next time.