Smashinglabs

Sebastian Poręba's blog

Unit testing for jQuery plugins – part 3

In part 1 I described basics of unit testing in JS, and part 2 was focused on CSS tests. It was an introduction to what I’m about to show now, so don’t forget to read these too. jQuery plugin testing means for me having to deal with a lot of unknowns. You don’t know how long code execution will take, you don’t know how other plugins you depend on work internally and you certainly never know when AJAX call or async function will end.

Asynchronous testing in QUnit

The most important part to understand is how to deal with code that is asynchronous. Consider this example:

test('addTwo test', function() {
   elem.slideDown();
   equal(elem.height(), 200);
}

In this very simple piece of code very common problem is hidden. There are plenty of such functions, related to animating elements, showing popups, highlighting, querying data, waiting for loading data and so on. They all share one concept – the call is made, function is working on something, but the rest of the code is executed at the same time. It’s considered to be very good – lets us code non-blocking scripts and provides means of creating very active content. But what we lose here is the ability to use a result of function immediately after its call. In this case, for test purposes.

Asynchronous – the good way

The good way of dealing with such code is a great example of why writing tests is so important. It always reminds me hacking – it forces you to think about all possible outcomes and to stretch your application to its limits. In this case writing tests shows what almost every plugin should have, an onComplete function. By providing it you may internally check if everything is finished and if other code may proceed. I decided that I need it when I discovered how long it takes for Google Maps to load markers with addresses instead of coordinates. This is a good practice that jQuery, jQuery UI and a most of other serious libraries use. Testing with onComplete function is simple:

test("map with address markers", function() {
    map = createNewMap();
    map.gMap({
        markers: [...], // some markers
        zoom: 12,
        onComplete: function() {
            data = map.data('gmap');
            ok(data.markers[0].getPosition().lat() && data.markers[0].getPosition().lng(), 'marker position correct');
        }
    });
});

This is a part of my gMap tests. Something is still missing here and you may have noticed. Using an onComplete function does not prevent code from moving on. Plugin init is called and immediately test function is finished. Depending on your testing framework it may still work, assertion will be called eventually and its result will be shown. The problem is, that meanwhile the state of your testing suite may change. You can see here, that my test starts with createMap() function, which clears old map and creates a new one. This is an obvious case where running two tests at the same time can’t work. QUnit solves it like that (explanation in code comments):

test("map with address markers", function() {
    map = createNewMap();
    map.gMap({
        markers: [...], // some markers
        zoom: 12,
        onComplete: function() {
            data = map.data('gmap');
            ok(data.markers[0].getPosition().lat() && data.markers[0].getPosition().lng(), 'marker position correct');
            start(); // let other tests continue
        }
    });
    stop(); // when the end of the function is reached, do not run any other test until start() is called
});

Asynchronous – the ugly way

The problems start when you are depending on some function that doesn’t support onComplete callback. For example, if you open an popup in Google Maps, animation takes some time but there is no callback (sorry for referring to maps all the time, but it’s what I know best). It makes sense from their point of view, as nobody sane would ever call anything when html is loaded to a popup. Well, but I don’t care too much about my sanity and I had to, so what now? This is quite long listing, but shows the popup problem described above.

test("infowindow", function() {
    map = createNewMap();
    map.gMap({
        markers: [
            {
                latitude: 50.083,
                longitude: 19.917,
                html: '<div class="test_marker">marker 1</div>'
            }
        ],
        zoom: 12,
        onComplete: function() {
            data = map.data('gmap');
            equal(data.infoWindow, null, 'infowindow empty');
            equal($('.test_marker').size(), 0, 'no .test_marker');
            google.maps.event.trigger(data.markers[0], 'click');
            window.setTimeout(function() {
                ok(data.infoWindow, 'infowindow set');
                equal($('.test_marker').size(), 1, '.test_marker present');
                start();
            }, 1000);
        }
    });
    stop();
});

The dirty hack here is calling a test in setTimeout. It works almost the same way as onComplete, except for the part where you have to guess how long may it take. You have to be very careful with that. If your guess is to low, your test may fail at random. Also you should think, whether too long execution time may affect normal user. If so, you should either document it very well or think about some other way of implementing it. Using our example, you may push the popup problem forward to the user and let him ignore it or fix it, or you may create another wrapper with onComplete callback. Considering performance and code length I usually choose the first option.

Accessing internal parts of plugin

A good plugin is not changing your global variables. It’s cool, but causes a problem when you want to test internal functions. Testing input and output of plugin is fine, but without checking some mid-results you will quickly find that finding and error is not as simple as we would like it to be.

Now, if you read something about jQuery plugin development you probably already have methods array and a nice dispatcher that lets you call $(selector).myPlugin(‘methodName’, arguments);. It seems like a perfect way to expose plugin internals and usually it is. But in case of testing it really isn’t. Keep in mind that these methods are useful AFTER initialization. There is really no good way of testing what happens inside, without any overhead. The options I considered are:

Debug mode

You may put in your config object an debug callback. Then call it in any place you want, like that:

methods = {
    _someInternalMethod: function() {
        // do something
        if(opts.debug) { opts.debug({
            //object with debug data
        }); }
    }
}

However, this makes your code look like crap and with big plugins every line of code counts. In gMap I use log option, but in a very limited way.

Use $.data()

This is something that I hesitated about for a long time. $.data() binds any desired data to your DOM element. I’m a performance freak, so it always gives me goosebumps. DOM interaction is very slow, can’t be compiled as far as I know and it’s always kinda weird. It’s against MVC, storing data in view when you want to use them in controller. However, jQuery plugins are usually designed to be stateless, or at least to hide theirs state. This is one of the fastest ways to expose your internal data and I use it sometimes. I’m not going to put any new example here, you can see use of $.data() in almost each previously shown. It requires some work to ensure that exposed data are always correct, especially when something internal changes, but here tests becomes handy and saves a lot of time. If you use $.data(), prepare a lot of them and check everywhere if it’s working. Also try to use the fastest method of data binding which is, at the moment, $.data(jQueryObject, key, value).

Turn your plugin into a builder

Builder is a very good design pattern. It works like a stack – you can add some config options with provided methods and call execute() or build() when needed. This is how I should have designed gMaps to begin with  and now with every version I introduce more and more functions for changing map setup. It’s very useful for both your users and testing.

Time for an overkill example:

test("changeSettings - fit new center", function() {
    map = createNewMap();
    var markers = [
                {
                    latitude: 50.083,
                    longitude: 19.917
                }
            ];
    var markers2 = [
                {
                    latitude: 50.083,
                    longitude: 19.917
                },
                {
                    latitude: 50.20917,
                    longitude: 19.75435
                },
                {
                    latitude: 50.502343,
                    longitude: 19.91243
                }
            ];
    map.gMap({
        markers: markers,
        zoom: 9,
        latitude: "fit",
        longitude: "fit",
        onComplete: function() {
            map.gMap('removeAllMarkers');
            window.setTimeout(function() {
                map.gMap('addMarkers', markers2);
                window.setTimeout(function() {
                    map.gMap('changeSettings', {
                      latitude: 'fit',
                      longitude: 'fit'
                    });
                    window.setTimeout(function() {
                        var center = map.data('gmap').gmap.getCenter();
                        ok(Math.abs(center.lat() - (50.083 + 50.502343)/2) < 0.001, 'center latitude correct');
                        ok(Math.abs(center.lng() - (19.917 + 19.75435)/2) < 0.001, 'center longitude correct');
                        start();
                    },1000);
                },1000);
            },1000);
        }
    });
    stop();
});

Here all mentioned methods are combined. One array of markers is added, then removed, second one is added and new center calculated. And the best part is that at the every mentioned stage you can add test coverage. I don’t do it, as I have separated tests for these things, but it’s possible.

This is probably the best way of dealing with testing and plugins in general. If you or your user at any moment would like to alter configuration, it is the proper way to deal with it. And I’m so confident about that, because for a long time I encouraged people to get Google Map object and markers from $.data() and manipulate them manually. It’s not very effective, it’s slow and forces user to know a lot about programming in general. Your users are not always newbies, but if they are, there is no way they could learn advanced stuff fast enough. Encourage people to find out how your code works, but never force them to do so.

Bonus: continuous integration

Just a quick note here. These are two very good tools that you can integrate with your git, or any other, not as good, version control.

JS test driver

The image is quite self-explanatory. You push your changes to the repository, JS Test Driver reads source and config, on its server multiple browsers are started, tests are runned and result is sent back. Simple and genius, you don’t have to start every browser manually and wait for test results.

http://code.google.com/p/js-test-driver/

Testling

Testling is built on the same concept, but is provided as an SaaS. You send test file in specified format to Testling via wget and you get your reports back. It’s a paid service, but provides a free account for limited CPU time (30 min, at the moment). If you don’t want to setup your JS Test Driver, which usually requires separate machine, try Testling.

http://testling.com/

Summary

This is the end of the series, I hope you liked it. On my github you can find code for the first two parts. For this one you have to download gMap and tests are here.


  • RSS
  • Facebook
  • Twitter

FAQ about Wordpress

This came as a surprise for me but gMap is ...

gMap 3.3.3 released

It was a looong time since I last visited gMap. ...

Talks for Google Dev

Two new slide decks appeared in lectures tab. This time with ...

Talks and lectures w

Every now and then I spend a weekend watching various ...

3D Tetris with Three

In the fifth part of tutorial we add some final ...

FAQ about Wordpress

This came as a surprise for me but gMap is ...

gMap 3.3.0 released

Christmas came early! New version of gMap is ready!

Lecture for GTUG: Ja

Today I gave a lecture for GTUG Krakow about optimizations in ...

Unit testing for jQu

In part 1 I described basics of unit testing in ...

Unit testing for jQu

In part 1 I described some basic concepts behind unit ...