Quantcast
Viewing all articles
Browse latest Browse all 68

Unit testing Kendo UI DataSources with Jasmine and Mockjax

 Image may be NSFW.
Clik here to view.
empty-test

A big part of using the MVVM pattern is separation of concerns, and to make testable code. Why is it then that ViewModels in Kendo UI apps are often left untested?

For me, I always find myself trying to test parts of my ViewModel, until I end up having a direct reference to a DataSource in it. For example:

dataSource = new kendo.data.DataSource({
    autoSync: true,
 
    transport: {
        read: {
            url: "api/people",
            type: "GET",
            dataType: "json"
        },
        create: {
            url: "api/people",
            type: "POST",
            dataType: "json"
        },
        destroy: {
            url: "api/people",
            type: "DELETE",
            dataType: "json"
        }
    },
     
    schema: {
        model: {
            id: "id",
            fields: {
                id: { type: "number" },
                name: { type: "string" }
            }
        }
    },
     
    error: function (errors) {
        viewModel.set("hasError", true);
    }
});
 
viewModel = kendo.observable({
    people: dataSource,
     
    hasError: false,
     
    peopleCount: function () {
        return this.people.view().length;
    }
});

At this point I usually wasn’t sure how to test with the DataSource being there, since it would attempt to get data from the server. Then I would just give up on testing (shame on me!)

So, how can we write good unit tests when there is a Kendo UI DataSource involved? With Mockjax!

Prerequisites and Setup

We are going to write some tests around the DataSource and ViewModel above. You can use your JavaScript testing framework of choice, but I am going to use Jasmine. Either Jasmine 2.0 or Mocha will have almost exactly the same tests, since their syntax is almost the same, and the way they handle asynchronous tests is the same.

We will use the mockjax library to mock our jQuery AJAX calls, which will make the Kendo DataSource think it is communicating with a server, but it is in fact being intercepted by mockjax.

Setup your Jasmine test runner to import jQuery, Kendo, and Mockjax. Note that Kendo and Mockjax are both dependent on jQuery, so it needs to be imported first. Then you can import your own text files. In this example, the test spec file is named “DataSourceTesting.spec.js”

<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="jquery.mockjax.js"></script>
<script type="text/javascript" src="kendo.ui.core.min.js"></script>
 
<!-- include spec files here... -->
<script type="text/javascript" src="spec/DataSourceTesting.spec.js"></script>

Getting the ViewModel

One of the frequent problems with writing testable JavaScript is heavy use of singletons or object literals. For example, simply specifying viewModel = kendo.observable({…}); can be a problem to test, because now we only ever have 1 viewModel object. If a test modifies any field on this object, it needs to be reset before the next test. Forgetting to do so leads to side-effects from running a test.

There are several ways to deal with this, but for this example, I am simply going to define a function that returns a new, unmodified ViewModel obejct for each test.

function createViewModel() {
    var dataSource = new kendo.data.DataSource({
        autoSync: true,
     
        transport: {
            read: {
                url: "api/people",
                type: "GET",
                dataType: "json"
            },
            create: {
                url: "api/people",
                type: "POST",
                dataType: "json"
            },
            destroy: {
                url: "api/people",
                type: "DELETE",
                dataType: "json"
            }
        },
         
        schema: {
            model: {
                id: "id",
                fields: {
                    id: { type: "number" },
                    name: { type: "string" }
                }
            }
        },
         
        error: function (errors) {
            viewModel.set("hasError", true);
        }
    });
     
    var viewModel = kendo.observable({
        people: dataSource,
         
        hasError: false,
         
        peopleCount: function () {
            return this.people.view().length;
        }
    });
     
    return viewModel;
};

Testing Read Operations

The basic layout for our test specs is:

describe("PeopleViewModel", function() {
    var viewModel;
 
    beforeEach(function() {
        viewModel = createViewModel();
    });

 

    // test go here
 
});

So you can see that a new ViewModel object will be created before each test. This ensures the tests don’t have side-effects.

Now lets define our first test; We want to test the peopleCount function on the ViewModel.

it("peopleCount should return number of people", function (done) {
    viewModel.people.one("change", function () {
        expect(viewModel.peopleCount()).toEqual(2);
        done();
    });
     
    viewModel.people.read();
});

This test will cause an error, because the DataSource will request data from the “api/people” URL when its .read() function is called. This is where we can use Mockjax to intercept that AJAX call, and return a known set of data. We do this using the $.mockjax() function to setup URLs to intercept.

it("peopleCount should return number of people", function (done) {
    $.mockjax({
        url: "api/people",
        type: "GET",
        dataType: "json",
        contentType: "application/json",
        responseText: [
            { id: 1, name: "John" },
            { id: 2, name: "Bob" }
        ]
    });
     
    viewModel.people.one("change", function () {
        expect(viewModel.peopleCount()).toEqual(2);
        done();
    });
     
    viewModel.people.read();
});

Now when DataSource.read() is called, Mockjax sees that the “apipeople” URL should be intercepted for “GET” requests, and returns the data assigned to “responseText” to the DataSource.

Since AJAX calls are still asynchronous, we use the Jasmine 2.0 “done” callback function. We also need to wait for the DataSource to signal that it is done reading, which is accomplished by waiting for the next “change” event.

Mockjax Clear – Avoid side effects

Since we added a mockjax mock during our test, we need to remove that before our next test, otherwise our mocked ajax handlers will persist to the next test and could have nasty side-effects.

You can remove all mockjax handlers by calling $.mockjaxClear() in the afterEach callback in Jasmine, so that it will be run after each test.

describe("PeopleViewModel", function() {
    ...
     
    afterEach(function () {
        $.mockjaxClear();
    });

Testing Create and Update

That works great for reads, but what about creates and updates? When you create a new record, Kendo UI wants you to leave the default value for whatever is specified as the schema.model.id field. In our example case, “id” is a number, so we should leave it set to 0 to indicate a new record. This will then be sent up to the server, and the server can reply with the same object that was passed to it, but with the id filled in. Let’s make sure this works.

it("add should get id from server", function (done) {
    $.mockjax({
        url: "api/people",
        data: { id: 0, name: "Tom" },
        type: "POST",
        dataType: "json",
        response: function() {
            this.responseText = { id: 3, name: "Tom" };
        }
    });
     
    viewModel.people.one("sync", function () {
        expect(viewModel.peopleCount()).toEqual(1);
        expect(viewModel.people.view()[0].id).toEqual(3);
        done();
    });
     
    viewModel.people.add({ id: 0, name: "Tom" });
});

This time we have our mockjax handler set up to watch for “POST”s to the URL, and to only intercept the AJAX call when an exact data item is posted to it. It will then reply with an object with a known id of 3 set on it.

We wait for the next “sync” event from the DataSource to know that it has processing the response from the server, and can then assert that the id of the object was changed to what was returned by the server.

Also notice that where our last test used “responseText” in the mockjax settings, this test uses “response” which is set to a function. This function just sets “this.responseText” to the data that should be sent back to the client, but I wanted to include it to show that you could do additional processing in this function if needed.

Testing Deletes

We can test deletes much the same way, however in our previous tests, we waited for the DataSource to finish processing the server response, then made some assert on the data. In this test, I demonstrate how you can run your asserts and call Jasmine “done()” directly from the mockjax “response” callback function if desired.

it("remove should send correct id to server", function (done) {
    $.mockjax({
        url: "api/people",
        data: { id: 3, name: "Tom" },
        type: "DELETE",
        dataType: "json",
        response: function() {
            expect(viewModel.peopleCount()).toEqual(0);
            done();
        }
    });
     
    viewModel.people.data([{ id: 3, name: "Tom" }]);
    viewModel.people.remove(viewModel.people.data()[0]);
});

Testing Errors

You may have noticed that when the DataSource that we have been using for these tests was defined, it had an error handler:

var dataSource = new kendo.data.DataSource({
    ...
     
    error: function (errors) {
        viewModel.set("hasError", true);
    }
});
 
var viewModel = kendo.observable({
    ...
     
    hasError: false,
     
    ...
});

Fortunately, Mockjax provides are really easy way to simulate a server error. Instead of setting “responseText” in the mockjax settings, you can use “status” instead to set an HTTP response code. If we set “status:500″ then the Kendo DataSource will run the error handler, and we can test that the “hasError” flag is set properly on the ViewModel.

it("should set hasError to true on server error", function (done) {
    $.mockjax({
        url: "api/people",
        type: "POST",
        dataType: "json",
        status: 500
    });
     
    viewModel.people.one("error", function () {
        expect(viewModel.hasError).toEqual(true);
        done();
    });
     
    viewModel.people.add({ id: 0, name: "Tom" });
});

Troubleshooting

If you have issues where Mockjax returns undefined to your DataSource instead of real data, make sure that you have dataType:”json” set on BOTH your DataSource and in the Mockjax options. I found that if one or the other was missing, then Mockjax would return either undefined, or just a string representing the JSON data, but Kendo would not JSON.parse it.

When mockjax intercepts AJAX calls properly, it will log them to the console:

MOCK GET: api/people > Object {url: "api/people", type: "GET", isLocal: true, global: true, processData: true…}
MOCK POST: api/people > Object {url: "api/people", type: "POST", isLocal: true, global: true, processData: true…}
MOCK DELETE: api/people > Object {url: "api/people", type: "DELETE", isLocal: true, global: true, processData: true…}
MOCK POST: api/people > Object {url: "api/people", type: "POST", isLocal: true, global: true, processData: true…}

Now you have one less excuse not to test your Kendo code! Go forth and test!

The post Unit testing Kendo UI DataSources with Jasmine and Mockjax appeared first on Falafel Software Blog.


Viewing all articles
Browse latest Browse all 68

Trending Articles