Test Driven Single Page Web Applications

In this post I will show an easy way to drive the design of composable single page web applications by unit tests.

What is a Single Page Web Application?

I was introduced to this concept few months ago while working in Moesion by Chris Love. Probabily you have never heard this name but for sure you are using a lot of web applications that uses this concept.

To put it simple, it is a web application that fits in only a single page. All the sections or pages of the application, along with the javascript, css, etc are downloaded at once or the application do it in background. The javascript logic is in charge of hiding or showing some sections of the page as the user navigates and fetching DATA from the backend and rendering it in the browser. This means that in this kind of application there is no that much server-side template rendering but client side.

To give you an example of this kind of application you can have a look to GMail. You will notice at a first glance that while the page may take a while to load the first time, the in-page navigation is super fast. If you look at the address bar while switching from the Inbox to the Compose page you will see something like this:

  • https://mail.google.com/mail/?zx=yadayada&shva=1#inbox
  • https://mail.google.com/mail/?zx=yadayada&shva=1#compose

The URL changes yes, but the part of the url that is changing is the hash part and this is client side. It means that all the navigation in gmail is happening within the boundaries of your browser and at the same time you still have this “deep linking” capabilities, to put in other words; I can easily create a bookmark menu to my gmail “Compose” page or screen.

The example domain

For the purpose of this article I will create a GMail clone, which I have randomly named “JMail”. We will focus on some basic features like the inbox and the compose page :).

Composable” single page web applications

Imagine that we are going to serve to the users a page like the following:

<html>
    <head>
        <title>JMail! like the other but better</title>
        <script type="text/javascript" src="..."></script>
    </head>
    <body>
        <div id="sidebar">
            <ul>
                <li><a href="...">InBox</a></li>
                <li><a href="...">Compose Mail</a></li>
                <li><a href="...">Sent</a></li>
                <li><a href="...">Trash</a></li>
            </ul>
        </div>
        <div id="inbox">
            ...
        </div>
        <div id="compose" style="display:none">
            ...
        </div>
        <div id="sent" style="display:none">
            ...
        </div>
        <div id="trash" style="display:none">
            ...
        </div>
    </body>
</html>

As you can see the application will fit in a single page. The different sections or screens of the application are just divs. This is the idea behind the pattern.

I have been thinking and I reached the following conclusion:

  • The fact that we are serving to the users a big fat html document doesn’t mean that we will code in a single html file. This seems very clear for me, if you are working on a real application you will have many pages, and just navigating the file is very hard. Also you have to think in things like source versioning, if you work on a team with other people you will easily run in conflicts. This means that we need a clear way to code and “compose” the single html file at some point. Almost every template engine or code generation tool has something like “sub-template” or what in the razor jargon we call “partials views”.
  • The fact that we need to compose the single file doesn’t mean that we have to do it at runtime. If you are doing ASP.Net this might sound weird to you, but after a while, working in this way I realized that we were using a very little of razor and that what I was using it could be done at build time rather than at runtime. I know that razor has a cache, and the output cache and all that, but serving static content is something way beyond that (numbers? I don’t have yet.). This is the reason why I wrote RazorCandle, another very very interesting project that I will consider is middleman.

Having said that, ultimately I am going to have a Compose.cshtml, Inbox.cshtml, maybe even a sidebar.cshtml.

Tools

The tools that I will use:

Our first test!

The first test I write is this:

module("Inbox");

test("can load the inbox mails", function() {
    //arrange
    this.server.respondWith("/getLastestMails",
                            [200, { "Content-Type": "application/json" },
                                JSON.stringify([{
                                    mailId: 123, 
                                    subject: "greetings from China", 
                                    from: "mom@foo.bar"}
                                ])]);
    //act
    jmail.inbox.loadPage();
    this.server.respond();

    //assert
    $("div#inbox")
        .assertContains("li.mail", "it should contains a li.mail");

    $("li.mail")
        .assertContainsText("greetings from China", "the mail item should show the subject somehow")
        .assertContainsText("mom@foo.bar", "the mail item should the mail somehow");
});     

The first thing that I did in this test is to mock a server call; it means that “/getLastestMails” will return this fixed JSON. I am using Sinon.Js for doing this.

The second thing is to actually execute “jmail.inbox.loadPage()” something that we don’t have yet.

And at the last part I wrote three asserts:

  • the div with id “inbox” should contains a “li” with class “mail”.
  • the “li” with class “mail” should contain the “subject” of the mail.
  • the “li” with class “mail” should contain the “from” of the mail.

these assertContains and assertContainsText are my own extensions to qunit, and I will talk about them in a minute.

Of course that at this point our first test is failing because I don’t have any functionality in place yet.

The project structure

Before continue let me explain the project structure a little bit:

2011-11-13_1102

  • pages: where the pages or sections of the application resides. Usually these are razor templates.
  • scripts: all the javascript files.
  • templates: here I put jquery templates, templates that are run in the client-side, usually in the form of razor files.
  • Tools: some scripts and executable that I use during development.
  • scripts/tests: in this folder I put all the qunit stuff, mock framework, tests extensions and qunit test harness.
  • scripts/tests/fixture: in this folder I only put the javascript fixtures.
  • scripts/tests/all.tests.cshtml: is a very importan file. It is the template for my qunit test harness (all.tests.html). I do this way because I will include in this file the razor templates that my application is going to use.
  • tools/qunit-runner/continuous.ps1: is a powershell script. it is monitoring the directory tree of the project continuously and whenever it detects a change in one of the files, it does two things:
  • scripts/tests/fixtures/inbox.tests.js where our first test resides.

The qunit test harness

The qunit test harness is:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
                    "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <script src="../jquery-1.6.2.js" type="text/javascript"></script>
        <script src="../jQuery.tmpl.js" type="text/javascript"></script>
        <script src="../jQuery.blockUI.js" type="text/javascript"></script>
        <script src="../common.js" type="text/javascript"></script>
        <script src="../json2.js" type="text/javascript"></script>
        <script src="../jmail.inbox.js" type="text/javascript"></script>

        <script src="qunit.js" type="text/javascript"></script>
        <script src="sinon-1.2.0.js" type="text/javascript"></script>
        <script src="sinon-qunit-1.0.0.js" type="text/javascript"></script>
        
        @if((bool)@Model.CI){
            <script src="Qunit.Teamcity.js" type="text/javascript"></script>
        }else{
            <script src="qunit.continuous.js" type="text/javascript"></script>
        }
        <script src="Util.Tests.js" type="text/javascript"></script>
        <link rel="stylesheet" href="qunit.css" type="text/css" media="screen" />
    </head>
    <body>
        <h1 id="qunit-header">QUnit tests</h1>
        <h2 id="qunit-banner"></h2>
        <div id="qunit-testrunner-toolbar"></div>
        <h2 id="qunit-userAgent"></h2>
        <ol id="qunit-tests"></ol>
        <div id="qunit-fixture">
            <div id="container">
                @Html.Partial(@"..\..\pages\compose.cshtml")
                @Html.Partial(@"..\..\pages\inbox.cshtml")
            </div>
        </div>
        
        @Html.Partial(@"..\..\templates\mail-templates.cshtml")

        @foreach (var file in System.IO.Directory.GetFiles("fixtures\\", "*.js")
                                       .Select(f => f.Replace("\\", "/")))
        {
            <script type="text/javascript" src="@file"></script>
        }
    </body>
</html>

Line by line:

  • from 5 to 9; the scripts that I will use in the real page.
  • line 10; jmail.inbox.js one of our core javascript files for this application.
  • from 12 to 14; qunit and sinon libraries.
  • from 16 to 20; Qunit.Continuous.Js and QUnit.Temacity.js, basically both files hook to the qunit framework and display important information to the console. Continouos is meat to be used when developing locally with human readable messages, and the second one display clunky messages that only Teamcity can read :)
  • line 21: some unit testing extensions (assertContains kind of thing)
  • line 22: the stylesheet for the qunit runner.
  • from 25 to 29: not that important, some containers that qunit uses to show stuff.
  • from 30 to 35: this is VERY important. Here I put all my application sections, because the div=”qunit-fixture” has a very important behavior; qunit will restore this dive after each run and because our tests are going to modify the DOM we need it.
  • line 37; include the mail-templates, which contain Jquery templates basically.
  • from 39 to 43; include all the fixture files using a convention.

Fixing the first test

To fix the first test we can implement something like this in jmail.inbox.js:

(function(){
    
    this.loadPage = function(){
        $.post("/getLastestMails")
         .done(function(mails){

            $("#inboxMailTemplate")
                .tmpl(mails)
                .appendTo("div#inbox > ul"); 

         });
    };

}).bind(namespace("jmail.inbox"))();

Basically the loadPage does a POST request to get the list of emails. The result of the POST function is a promise, so we subscribe to the done event, and then with the result we render the “inboxMAilTemplate” and append this to the div with id equals to inbox.

The inboxMailTemplate is inside the  mail-templates.cshtml file:

<script id="inboxMailTemplate" type="text/x-jquery-tmpl">
    <li class="mail"><b>${from}</b> ${subject}</li>
</script>

After these two changes we will see in the continuous testing window this:

2011-11-13_2131

What if the inbox is empty?

test("when loading an empty inbox then show a message", function() {
    this.server.respondWith("/getLastestMails",
                            [200, { "Content-Type": "application/json" },
                             JSON.stringify([])]);
    
    jmail.inbox.loadPage();

    this.server.respond();

    $("div#inbox")
        .assertNotContains("li.mail", "it should contains a li.mail");

    $("div#inbox")
        .assertContainsText("The inbox is empty");
});  

the console now looks as:

 2011-11-13_2142

to fix this we need some changes in our jmail.inbox.js:

(function(){
    
    this.loadPage = function(){
        $.post("/getLastestMails")
         .done(function(mails){
            if(mails.length === 0){
                $("#inboxEmptyTemplate")
                    .tmpl()
                    .appendTo("div#inbox > ul");
            }else{
                $("#inboxMailTemplate")
                    .tmpl(mails)
                    .appendTo("div#inbox > ul");
            }
         });
    };

}).bind(namespace("jmail.inbox"))();

and in the mail-templates.cshtml we add a new template:

<script id="inboxEmptyTemplate" type="text/x-jquery-tmpl">
    <li>The inbox is empty</li>
</script>

Display a loading message while loading the list of emails

 

test("while loading the inbox it should show a loading message", function() {
    var blockUiStub = this.stub($, "blockUI");

    jmail.inbox.loadPage();

    ok(blockUiStub.called);
    equal(blockUiStub.args[0][0].message, "Loading mails, please wait.");
});

 

here we are using the stub functionality of sinon.js, it replaces the method with something you can change or assert it was called with some parameters. One of the interesting things about sinon.js is that it has a SandBox api, so when you kill the sandbox it will restore all the methods. The sandbox is automatically created and destroyed with the sinon-qunit integration.

To make this test pass, we add a call to $.blockui({message: “Loading mails, please wait.”} before the post. We will need also to make sure we are unblocking the UI after the server respond:

test("after loading the inbox it should unblock the ui", function(){
    this.server.respondWith("/getLastestMails",
                            [200, { "Content-Type": "application/json" },
                                JSON.stringify([])]);
    
    var blockUiStub = this.stub($, "unblockUI");
    
    jmail.inbox.loadPage();
    this.server.respond();
    ok(blockUiStub.called);
});

so our load page method now looks like

(function(){
    
    this.loadPage = function(){
        $.blockUI({message: "Loading mails, please wait."});
        
        $.post("/getLastestMails")
         .done(function(mails){
            if(mails.length === 0){
                $("#inboxEmptyTemplate")
                    .tmpl()
                    .appendTo("div#inbox > ul");
            }else{
                $("#inboxMailTemplate")
                    .tmpl(mails)
                    .appendTo("div#inbox > ul");
            }
         }).always($.unblockUI);
    };

}).bind(namespace("jmail.inbox"))();

Navigation tests

For the navigation I am going to use the JQuery BBQ pluggin, we can start with these two tests:

module("Navigation tests");

test("when navigating to the compose screen then push target=compose", function() {
    //arrange
    var bbqStub = this.stub($.bbq, "pushState");
    
    //act
    $("a#compose").click();

    //assert
    ok(bbqStub.called);
    equal(bbqStub.args[0][0].target, "compose" );
});

test("when navigating to the inbox screen then push target=inbox", function() {
    //arrange
    var bbqStub = this.stub($.bbq, "pushState");
    
    //act
    $("a#inbox").click();

    //assert
    ok(bbqStub.called);
    equal(bbqStub.args[0][0].target, "inbox" );
});

then the implementation in a new jmail.events.js file is:

$(document)
.delegate("#compose", "click", function(e){
    e.preventDefault();

    $.bbq.pushState({
        target: "compose"
    });

    return false;
}).delegate("#inbox", "click", function(e){
    e.preventDefault();

    $.bbq.pushState({
        target: "inbox"
    });
    
    return false;
});

The next thing we will handle, is hiding the pages when navigating:

test("when changing the hash to target=inbox then compose should be hide", function() {
    //arrange
    var getState = this.stub($.bbq, "getState");
    getState.withArgs("target").returns("inbox");
    $("div#compose").show();

    //act
    $(window).triggerHandler("hashchange");

    //assert
    var isComposeVisible = $("div#compose").is(":visible");
    ok(!isComposeVisible);
});

of course we can start with something like

$(window).bind("hashchange", function(e){
    var target = $.bbq.getState("target");
    if(target === "inbox"){
        $("div#compose").hide();   
    }
});

but then we will need to handle the other way

test("when changing the hash to target=compose then inbox should be hide", function() {
    //arrange
    var getState = this.stub($.bbq, "getState");
    getState.withArgs("target").returns("compose");
    $("div#inbox").show();

    //act
    $(window).triggerHandler("hashchange");

    //assert
    var isInboxVisible = $("div#inbox").is(":visible");
    ok(!isInboxVisible);
});

so we can now generalize the behavior to something like this:

$(window).bind("hashchange", function(e){
    var target = $.bbq.getState("target");
    $("div#container > div").hide();
    $("div#" + target).show();
});

another thing that we should handle now is the call to the loadPage when the hash change:

test("when changing the hash to target=inbox then call the inbox.loadPage", function() {
    //arrange
    var getState = this.stub($.bbq, "getState"),
        jmailLoadPage = this.stub(jmail.inbox, "loadPage");

    getState.withArgs("target").returns("inbox");

    //act
    $(window).triggerHandler("hashchange");

    //assert
    ok(jmailLoadPage.called);
});

we can generalize the behavior in a convention as follows:

$(window).bind("hashchange", function(e){
    var target = $.bbq.getState("target");
    $("div#container > div").hide();
    $("div#" + target).show();
    if(jmail[target] && jmail[target].loadPage){
        jmail[target].loadPage();   
    }
});

Wiring it all up

We can wire all the components in an index.cshtml page as follows:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
                      "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <script src="scripts/jquery-1.6.2.js" type="text/javascript"></script>
        <script src="scripts/jQuery.tmpl.js" type="text/javascript"></script>
        <script src="scripts/common.js" type="text/javascript"></script>
        <script src="scripts/json2.js" type="text/javascript"></script>
        <script src="scripts/jquery.ba-bbq.js" type="text/javascript"></script>
        <script src="scripts/jquery.blockUI.js" type="text/javascript"></script>
        <script src="scripts/jmail.inbox.js" type="text/javascript"></script>
        <script src="scripts/jmail.events.js" type="text/javascript"></script>

        <!-- since i don't have a backend i need this to show something in the ui -->
        <script src="scripts/jquery.mockjax.js" type="text/javascript"></script>
        <script type="text/javascript">
        $.mockjax({
          url: '/getLastestMails',
          responseTime: 1500,
          responseText: [{ mailId: 123, subject: "greetings from China",  from: "mom@foo.bar"},
                          { mailId: 124, subject: "pay the tax!",  from: "wife@foo.bar"}]
        });
        </script>
        <!-- end mockjax-->

        <!--since the inbox is the first page
            this could be better!
         -->
        <script type="text/javascript">
        $(document).ready(jmail.inbox.loadPage);
        </script>
        <!--end first page loader-->

    </head>
    <body>
@Html.Partial(@"pages\menu.cshtml")
        <div id="container">    
@Html.Partial(@"pages\compose.cshtml")
@Html.Partial(@"pages\inbox.cshtml")
        </div>
    </body>
@Html.Partial(@"templates\mail-templates.cshtml")    
</html>

Conclusion

In this article I tried to show a way you can design single page web applications driven by unit tests. This is only a way you can work with html and javascript.  I have not used any MV** framework like Backbone.js or Knockout which will improve the design, but it wasn’t something that I missed for this example.

The repository has the full history of each step I made and it is hosted on my github here. Go browse the code, or fork it and try implementing another functionality while running the continuous.ps script in the background.

I hope you find this stuff useful.


blog comments powered by Disqus
  • Categories

  • Archives