Here’s a walk through of a Jasmine example for testing the functionality of the selector arrows show below.

Basically I want to test the jQuery selectors and button functionality. When I click the > button, I want to test that the contents of the ‘Default Trading Group’ box slide over to the ‘Available Trading Groups’ text box.

Step 1: Just get it working.

Before I Jasmine up any of my code, I find it useful just to get the UI working. I am not strong enough in Jasmine or Javascipt yet to TDD stuff like this out. So first pass through, I just hack enough to get the basic controls working, the jQuery selectors setup, and the basic functionality working.

Step 2: Define the model.

With the basic UI going, I was now ready to think about the design.

How did I want this thing to work?

I decided to start with a model (ala MVC). I wanted a model that I could talk to to get the state of any control I wanted on the page. I then wanted this model to be used by a controller (which would actually do this to the page).

Because I already got the page going, my model basically consisted of jQuery selectors to return things off the page. It looked something like this:

function UpdateTradingGroupModel() {
}


UpdateTradingGroupModel.prototype.defaultTradingGroupId = function() {
    return $('#TextDefaultTradingGroup').attr("defaultTradingGroupId");
};

UpdateTradingGroupModel.prototype.defaultTradingGroupName = function() {
    return $('#TextDefaultTradingGroup').val();
};

...

There’s more, but I think you get the gist of it. Anything I want to know the state of, I just talk to my model and it gives it to me.

And then here’s sampling of the controller:

function UpdateTradingGroupController(theModel) {
    this.model = theModel;
}

UpdateTradingGroupController.prototype.configureArrowButtons = function() {

    that = this; // important!

    $('#remove').click(function(){
        $('#SelectedTradingGroups option:selected').appendTo('#AvailableTradingGroups');
    });

    $('#add').click(function(){
        $('#AvailableTradingGroups option:selected').appendTo('#SelectedTradingGroups');
    });

...

    $('#save').click(function(){

        if (that.model.defaultTradingGroupIsEmpty()) {
            that.showWarningDialog();
        } else {
            that.updateTradingGroups(that.model);
        }
        
    });

};

UpdateTradingGroupController.prototype.updateTradingGroups = function(model) {

    params = model.saveParameters();

    $.ajax({
        type: "POST",
        traditional: true,
        url: getControllerAddress('User') + "/@Model.User.Id/UpdateTradingGroups2",
        data:params,
        dataType: 'json',
        success: function(result)
        {
            $('#SuccessMessage').html(result.message);
        }
    });
};

The controller basically sets up the buttons, and actually does the work (like saving any changes done to the page).

And I would add this functionality to the javascript header section of my webpage with something like this:

    var controller = new UpdateTradingGroupController(new UpdateTradingGroupModel());
    controller.configureArrowButtons();

Alright. With that out of the way, we can now look at some Jasmine tests.

Step 3: Create your Jasmine tests.

The first test I wanted to write verified that when a user clicked the > button, the text in the default trading group got removed and added to the available trading group.

That test looked something like this:

describe("UpdateTradingGroupSpec - When clicking the #removeDefault button", function(){

    var model;
    var controller;

    beforeEach(function() {

        setFixtures(
        '<input type="text" id="TextDefaultTradingGroup" defaulttradinggroupid="10" value="ctg1">' +
        '<input id="removeDefault" type="button" value=" &gt; ">' +
        '<select id="AvailableTradingGroups" multiple="multiple" name="AvailableTradingGroups"></select>');

        model = new UpdateTradingGroupModel();
        controller = new UpdateTradingGroupController(model);
        controller.configureArrowButtons();
        
    });

    it("should slide selected field from TextDefaultTradingGroup to available trading groups", function(){
        expect(model.defaultTradingGroupId()).toEqual('10');
        expect(model.availableTradingGroups()).toEqual([]);
        $('#removeDefault').click();
        expect(model.defaultTradingGroupId()).toEqual('');
        expect(model.availableTradingGroups()).toEqual(['10']);
    });

    it("should be able to determine if TextDefaultTradingGroup is empty", function(){
        expect(model.defaultTradingGroupIsEmpty()).toBeFalsy();
    });

});

The first thing to note is this setup fixture line:

        setFixtures(
        '<input type="text" id="TextDefaultTradingGroup" defaulttradinggroupid="10" value="ctg1">' +
        '<input id="removeDefault" type="button" value=" &gt; ">' +
        '<select id="AvailableTradingGroups" multiple="multiple" name="AvailableTradingGroups"></select>');

It’s beautiful because it enables us to take just the html we need from our page, and set it up however we want it. Just view source on your page, grab what you need and put it in here. You can also ‘loadFixture‘ which will load an entire html file and set that up as your test harnass if you like.

In this case I want the TextDefaultTradingGroup textbox to have an id and value of 10 and ctg1 respectfully (note this doesn’t match what’s on the screen), with a remove button, and select box called AvailableTradingGroups which is empty.

I can then configure the buttons on my page:

        model = new UpdateTradingGroupModel();
        controller = new UpdateTradingGroupController(model);
        controller.configureArrowButtons();

And always remember to include whatever javascript you use in your app in your jasmine test runner file (else it won’t find your javascript).

Now I am ready to test what happens when someone clicks the remove button.

By simply asking my model I can verify that things start like this:

        expect(model.defaultTradingGroupId()).toEqual('10');
        expect(model.availableTradingGroups()).toEqual([]);

Then they hit the removeDefault button

        $('#removeDefault').click();

And end up like this:

        expect(model.defaultTradingGroupId()).toEqual('');
        expect(model.availableTradingGroups()).toEqual(['10']);

It was a lot of working setting this up, but now that the tracks are laid, we can setup any configuration of buttons and values we want.

You could test every other button combination and configuration of data (we had bugs in there).

Or if you wanted to display a warning dialog in the event someone tries to save a blank default trading group you could write something like this:

describe("UpdateTradingGroupSpec - When clicking the #save button", function(){

    var model;
    var controller;

    beforeEach(function() {

        setFixtures(
        '<input type="text" id="TextDefaultTradingGroup" defaulttradinggroupid="10" value="ctg1">' +
        '<select id="AvailableTradingGroups" multiple="multiple" name="AvailableTradingGroups"></select>' +
        '<select id="SelectedTradingGroups" multiple="multiple" name="SelectedTradingGroups">' +
            '<option value="11">ctg2</option>' +
            '<option value="12">ctg3</option>' +
        '</select>' +
        '<input type="submit" id="save" value="Save">');

        model = new UpdateTradingGroupModel();
        controller = new UpdateTradingGroupController(model);
        controller.configureArrowButtons();

        spyOn(Application, 'rootDir').andReturn("/");
    });

     it ("should display a warning dialog if defaultTradingGroupIsEmpty", function() {
        spyOn(model, 'defaultTradingGroupIsEmpty').andReturn(true);
        spyOn(controller, 'showWarningDialog');
        $('#save').click();
        expect(model.defaultTradingGroupIsEmpty).toHaveBeenCalled();
        expect(controller.showWarningDialog).toHaveBeenCalled();
    });

    it ("should not display a warning dialog if TextDefaultTradingGroup has data", function() {
        spyOn(model, 'defaultTradingGroupIsEmpty').andReturn(false);
        spyOn(controller, 'showWarningDialog');
        $('#save').click();
        expect(model.defaultTradingGroupIsEmpty).toHaveBeenCalled();
        expect(controller.showWarningDialog).not.toHaveBeenCalled();
    });

    it ("should collect parameters for save", function() {
        expect(model.saveParameters()).toEqual({ defaultTradingGroupId : '10', selectedCompanyTradingGroups : [ '11', '12' ] });
    });

});

Or if I wanted to verify an ajax call was made to the backend I could write something like this:

    it ("should make an ajax call to do the save", function() {
        spyOn($, 'ajax').andReturn('Successful save.');
        controller.updateTradingGroups(model);
        expect($.ajax).toHaveBeenCalled();
    });

I’ve only just scratched the surf of what we can do with Jasmine. But I wanted to get a few examples up to show what it can look like.

One thing I still haven’t got a great grasp out yet is Javascript closures and use of ‘this‘ and ‘that‘.

If you are coming from an enterprise language background like C# or Java, just beware that object state and scope is different in Javascript which is why passing in a model to a controller in Javascript has lines like

UpdateTradingGroupController.prototype.configureArrowButtons = function() {

    that = this; // important!

I am still figuring it out and will write more once I do.

But give Jasmine a chance. It’s helped us reduce the number of bugs in our app and I am looking forward to writing many more.

If you are just starting out checkout Evan Hahn’s Jasmine tutorial. It’s easy peasy.