Unit Testing KnockoutJS and Web API

After a couple of years of looking on from the side-lines at the advancements being made world of web development, I decided recently it was time to dive in head-first and bring my web knowledge up to speed. A lot of my initial work with C# and .NET was with ASP.NET WebForms, but in the past few years the majority of my work has been either mobile, desktop, or server-based.

So, for the past couple of months I’ve been investigating a variety of topics from top-to-bottom, including HTML5 and CSS3, Bootstrap, ASP.NET MVC, Entity Framework (including Repository, Unit of Work, and Service patterns), Dependency Injection, JavaScript (including the Module and Revealing Module patterns) and jQuery, KnockoutJS, and finally QUnit.

One topic I thought I’d blog about is how to unit test client-side JavaScript code, specifically ViewModels used by KnockoutJS that communicate with a Web API endpoint. To help illustrate these techniques I’ve created an ultra-simple ASP.NET MVC application that uses both Web API and KnockoutJS. Shockingly, it’s a to-do application. You can check out the source code for the application here. You can also download a snapshot of the project, before adding unit testing, here.

Here is the main view for the MVC application:

<input data-bind="value: addingItemText, valueUpdate: 'afterkeydown'" type="text" />
<button data-bind="enable: canAddItem, click: addNewItem">Add</button>

<ol data-bind="foreach: items">
    <li>
        <strong data-bind="visible: Completed">Completed </strong>
        <span data-bind="text: Text"></span>
        <button data-bind="click: $root.deleteSelectedItem">Delete</button>
        <button data-bind="click: $root.completeSelectedItem, visible: !Completed()">Complete</button>
        <button data-bind="click: $root.undoSelectedItem, visible: Completed()">Undo</button>
    </li>
</ol>

@section scripts {
    @Scripts.Render("~/bundles/app")
}

I define a text input and button for adding a new to-do item. The Add button should only be enabled when there is text entered. Following that there is a list of to-do items. If an item has been completed it shows appropriate text in bold. There’s a button to delete an item. Finally, if the item is uncompleted there is a button to complete it, and if the item is completed there is a button to undo it.

Here is a look at the ViewModel:

$((function (ns, webApiClient) {
    "use strict";

    ns.todoViewModel = (function () {

        //utilities
        function cloneJSModel(sourceModel, destinationModel) {
            destinationModel.Id(sourceModel.Id)
                .Text(sourceModel.Text)
                .Completed(sourceModel.Completed);
        }

        function cloneKOModel(sourceModel, destinationModel) {
            var jsModel = ko.toJS(sourceModel);
            cloneJSModel(jsModel, destinationModel);
        }

        //UI binding
        var items = ko.observableArray();

        //web api calls
        function populate() {

            webApiClient.ajaxGet("TodoItem", "", function (json) {
                items.removeAll();

                $.each(json, function (index, value) { //ignore jslint
                    var item = new ns.todoItemModel();
                    cloneJSModel(value, item);
                    items.push(item);
                });
            });
        }

        function addItem(todoItem) {

            webApiClient.ajaxPost("TodoItem", ko.toJS(todoItem), function (result) {
                var newItem = new ns.todoItemModel();
                cloneJSModel(result, newItem);
                items.push(newItem);
            });
        }

        function deleteItem(id) {

            webApiClient.ajaxDelete("TodoItem", id, function (result) {
                items.remove(function (item) {
                    return item.Id() === result.Id;
                });
            });
        }

        function updateItem(todoItem) {

            webApiClient.ajaxPut("TodoItem", todoItem.Id(), ko.toJS(todoItem), function () {
                var existingItem = ko.utils.arrayFirst(items(), function (item) {
                    return item.Id() === todoItem.Id();
                });
                cloneKOModel(todoItem, existingItem);
            });
        }

        //UI actions
        var addingItemText = ko.observable('');

        var canAddItem = ko.computed(function () {
            return addingItemText() !== "";
        });

        var addNewItem = function () {
            var newItem = new ns.todoItemModel();
            newItem.Text(addingItemText());
            addItem(newItem);
            addingItemText("");
        };

        var deleteSelectedItem = function () {
            deleteItem(this.Id());
        };

        var completeSelectedItem = function () {
            this.Completed(true);
            updateItem(this);
        };

        var undoSelectedItem = function () {
            this.Completed(false);
            updateItem(this);
        };

        //return a new object with the above items
        //bound as defaults for its properties
        return {
            items: items,
            populate: populate,
            addingItemText: addingItemText,
            canAddItem: canAddItem,
            addNewItem: addNewItem,
            deleteSelectedItem: deleteSelectedItem,
            completeSelectedItem: completeSelectedItem,
            undoSelectedItem: undoSelectedItem
        };

    }());

    ns.todoViewModel.populate();

    ko.applyBindings(ns.todoViewModel);

    //pass in namespace prefix (from namespace.js)
}(todo, todo.webApiClient)));

Again this is all pretty standard. It follows the Revealing Module pattern for the ViewModel. One thing to note is that a webApiClient is passed in and used for the AJAX calls. John Papa shows something very similar in his Pluralsight training courses. This nicely abstracts out the Web API specifics plus, as you’ll see, it makes it easier to unit tests our ViewModel.

Here’s the Web API client source:

(function (ns) {
    "use strict";

    ns.webApiClient = (function () {

        var ajaxGet = function (method, input, callback, query) {

            var url = "/api/" + method;
            if (query) {
                url = url + "?" + query;
            }

            $.ajax({
                url: url,
                type: "GET",
                data: input,

                success: function (result) {
                    callback(result);
                }
            });
        };

        var ajaxPost = function (method, input, callback) {

            $.ajax({
                url: "/api/" + method + "/",
                type: "POST",
                data: input,

                success: function (result) {
                    callback(result);
                }
            });
        };

        var ajaxPut = function (method, id, input, callback) {

            $.ajax({
                url: "/api/" + method + "/" + id,
                type: "PUT",
                data: input,

                success: function (result) {
                    callback(result);
                }
            });
        };

        var ajaxDelete = function (method, id, callback) {

            $.ajax({
                url: "/api/" + method + "/" + id,
                type: "DELETE",

                success: function (result) {
                    callback(result);
                }
            });
        };

        return {
            ajaxGet: ajaxGet,
            ajaxPut: ajaxPut,
            ajaxPost: ajaxPost,
            ajaxDelete: ajaxDelete
        };
    }());

    //pass in namespace prefix (from namespace.js)
}(todo));

As you can see we’re simply wrapping access to the jQuery ajax function and calling our callback function. Again you can download the source code above or from Bitbucket and run the app to try all this out. It all works as expected: you can add, delete, complete, and undo items.

In this example I’ll be using QUnit to unit test the JavaScript. JavaScript unit tests, unlike standard unit tests, generally exist in the same project as your site. You’ll see that sites like KnockoutJS and Sugarjs have a webpages where you can run their tests. The tests need access to your JavaScript source files, and there is no easy way to make these available to other projects like you can .NET assemblies.

So we’ll start by creating a new Tests folder in the project.

Add New Folder

Inside that folder create both a test.html and a tests.js file. The next step is to put the QUnit specific markup in the tests.html file:

<!DOCTYPE html>
<html>
    <head>
        <title></title>
        <meta charset="utf-8">
        <!-- QUnit stylesheet from the jQuery CDN -->
        <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.11.0.css">
    </head>
    <body>
        <!-- elements that QUnit will inject test results into -->
        <div id="qunit"></div>
        <div id="qunit-fixture"></div>

        <!-- required JS libraries, such as jQuery and KnockoutJS -->
        <script src="/Scripts/jquery-1.9.1.js"></script>
        <script src="/Scripts/knockout-2.2.1.debug.js"></script>

        <!-- QUnit itself from the jQuery CDN -->
        <script src="http://code.jquery.com/qunit/qunit-1.11.0.js"></script>

        <!-- include tests themselves -->
        <script src="tests.js"></script>
    </body>
</html>

Note that I’ve also included references to the jQuery and Knockout js files. Next, add the following code to the tests.js file in order to ensure things are working:

test("hello test", function () {
    ok(1 == "1", "Passed!");
});

Now, if you right-click on the tests.html file and click View in Browser, you should see passing results for the single test.

Hello Test Run

Now lets implement actual unit tests for our ViewModel. One of the cardinal rules of unit tests is that they should not interact with the “outside world”, specifically things like databases, file systems, and web services. Tests that do this, even using a unit testing framework, are known as integration tests. So, one thing we need resolve is how to test our ViewModel without making the Web API calls.

We’ll do this by creating a stub for our Web API client object. With typical C# applications you can do stubbing, mocking, and faking in a variety of ways. I personally like using FakeItEasy. However, the dynamic nature of JavaScript means that, at least for something simple like stubbing our Web API client, there’s really no additional framework needed.

Let’s start the stubbing by creating a new JavaScript file called webapiclient.stub.js in the Tests folder. Now add the following code to define the stub:

(function (ns) {
    //better exceptions, less tomfoolery allowed
    "use strict";

    ns.webApiClient = (function () {

        var testResult = [];

        var ajaxGet = function (method, input, callback, query) { //ignore jslint
            callback(this.testResult);
        };

        var ajaxPost = function (method, input, callback) { //ignore jslint
            callback(this.testResult);
        };

        var ajaxPut = function (method, id, input, callback) { //ignore jslint
            callback(this.testResult);
        };

        var ajaxDelete = function (method, id, callback) { //ignore jslint
            callback(this.testResult);
        };

        //return a new object with the above items
        //bound as defaults for its properties
        return {
            ajaxGet: ajaxGet,
            ajaxPut: ajaxPut,
            ajaxPost: ajaxPost,
            ajaxDelete: ajaxDelete,
            testResult: testResult
        };
    }());

    //pass in namespace prefix (from namespace.js)
}(todo));

This code is pretty straight forward. It uses the same signatures as the real Web API client object. However, instead of making real AJAX calls, it calls the callback immediately, passing back the value stored in testResult.

One important thing to note is that the function calls to the callbacks pass this.testResult rather than just testResult. This is because the return block is returning a new object with the specified properties and default values for those properties. Those properties are not getters or setters for the privately scoped variables above, although it may look that way if you are used to OOP languages like C# or Delphi.

Next lets look at how to make use of this stub to write tests against the ViewModel. We’ll add the following script references to the tests.html file:

<!-- include the application's namespace.js -->
<script src="/Scripts/app/namespace.js"></script>

<!-- include our sub Web API client -->
<script src="webapiclient.stub.js"></script>

<!-- include the items to test -->
<script src="/Scripts/app/model.js"></script>
<script src="/Scripts/app/viewmodel.js"></script>

The first reference is to our Web API client stub and the second two are to our application’s Model and ViewModel respectively. Now lets add our first real test to the tests.js file:

module("todo.viewmodel.populate");

test("todo.viewmodel.populate (0 length)", function () {
    "use strict";

    //arrange
    todo.webApiClient.testResult = [];

    //act
    todo.todoViewModel.populate();

    //assert
    equal(todo.todoViewModel.items().length, 0, "Passed!");
});

The first line defines a module. This is not required at all and is merely a way to group tests visually into sections when results are displayed. Next we setup the todo.webApiClient (our stub) to return an empty array for any calls to it. Then, we call the populate() function on our ViewModel. Finally, we assert that the items() in our ViewModel is zero-length. You can save the tests.js and tests.html file and refresh your browser to view the results:

First Real Test Run

Here are some more example tests for the to-do ViewModel:


test("todo.viewmodel.populate (1 length)", function () {
    "use strict";

    //arrange
    todo.webApiClient.testResult = [
        {
            Id: 1,
            Text: "To-do",
            Completed: false
        }
    ];

    //act
    todo.todoViewModel.populate();

    //assert
    equal(todo.todoViewModel.items().length, 1, "Passed!");
});

module("todo.viewmodel.canAddNewItem");

test("todo.viewmodel.canAddNewItem (without text)", function () {
    "use strict";

    //arrange

    //act
    todo.todoViewModel.addingItemText('');

    //assert
    equal(todo.todoViewModel.canAddItem(), false, "Passed!");
});

test("todo.viewmodel.canAddNewItem (with text)", function () {
    "use strict";

    //arrange

    //act
    todo.todoViewModel.addingItemText('To-do');

    //assert
    equal(todo.todoViewModel.canAddItem(), true, "Passed!");
});

module("todo.viewmodel.addNewItem");

test("todo.viewmodel.addNewItem", function () {
    "use strict";

    //arrange
    todo.webApiClient.testResult = [];
    todo.todoViewModel.populate();

    var expectedItem = {
        Id: 1,
        Text: "To-do",
        Completed: false
    }

    todo.webApiClient.testResult = expectedItem;

    //act
    todo.todoViewModel.addingItemText(expectedItem.Text);
    todo.todoViewModel.addNewItem();

    //assert
    var firstItem = todo.todoViewModel.items()[0];
    equal(firstItem.Id(), expectedItem.Id, "Passed!");
});

Along with results for the full suite of tests:

All Tests Run

Hopefully this proves helpful to other developers taking a look at writing more client-side JavaScript and looking for ways to test it. Look forward to more posts in the coming months on other topics and techniques I’ve discovered relating to these evolving web technologies.

3 thoughts on “Unit Testing KnockoutJS and Web API

  1. Pingback: Running KnockoutJS Unit Tests with Chutzpah | blog.nwoolls.com

  2. Chris

    Excellent tutorial!

    I really appreciate your example going all the way down to your actual unit tests with all the included details. The ability to unit test the viewmodel while using knockout was a key piece of information that I’ve been missing.

    Best Regards,

    Chris

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>