Mad, Beautiful Ideas
Unit Testing JavaScript

I've been working lately on a fair amount of JavaScript on my own time. Not a ton, but enough to keep me thinking about using JavaScript in a meaningful way from a software development standpoint. One thing, which I'll admit to not being very good at, that I keep coming back to is the idea of unit-testing. Lately, I have two JavaScript modules that I've been working on, both of which have test cases in their systems, but both, I feel, fall short in one simple way: both sets of test cases have to run in the browser.

Why do I consider this to be a negative? It's hard to automate. It's difficult for me to have a script that opens up the browser, runs the tests on the page, and then dumps the results of those tests to a file which can be viewed or analyzed later, like if I wanted to run those tests as part of a Continuous Integration system. Which is important, since it serves as one final test before I publish any code.

Admittedly, there are unit-test frameworks for JavaScript jsunit, JSSpec, Screw.Unit, and my favorite YUI Test. The problem with these, from my perspective, is that they all only operate within the browser. To be fair, this is a reasonable compromise, as there are a lot of things that are done with JavaScript that make the most sense in the browser environment, but what I'm wanting to do is 'mock' the browser away, and write tests against a mock browser, which would make my unit tests more productive.

Part of this, is because Unit Testing in the browser lacks certain advantages I get in unit-testing other languages. In .NET, which I build unit-tests I can unit-test my private methods, which are usually the ones I'm going to care about. For instance, in the Laconi.ca widget in the sidebar, that reads my posts from the TWiT Army, I wanted to have three 'private' methods, _timePhrase (converts a time to "xx seconds ago" type messages), _userLink (converts the @user to a link to that user), and _uriLink (converts any URI to a anchor link). Because I can't unit-test private methods, I had to put them in the public return object. Incidentally, when YUI does this, they often use public wrapper functions around the private method to prevent monkey-patching. I'm considering doing the same.

I'm not going to go too into depth on YUI Test, as Nicholas Zakas, the guy who wrote it, put up a really good blog posting this week about using it. And, as for my Continuous Integration problem, it is possible to have YUI Test send it's results to a given URL so that we know how it's doing or not. Now, my only complaint is that I don't know an easy way to automate opening up the browser, and the test case file, and then closing the browser only after I know the test framework has ended. Any ideas on that one, I'd really appreciate.

Getting back to the Laconi.ca widget, I had initially begun testing the code like this:

        Foxxtrot.Widgets.Laconica.getUpdates('#army_update_list',
            {count: 3, user: 'foxxtrot',
             service_url: './data/'});
        

This told the widget to use the HTML test file's path's 'data' subdirectory as the site root to get a copy of the JSON output from laconica for user 'foxxtrot'. I used a local copy, so that I could be sure that the data didn't change, and that it had a reasonable set of test cases. All I'd do then, is just look at the output to see if I liked it. It worked, okay, but given my complaints about manually running unit-tests, it clearly wasn't going to satisfy me for long. So, following Zakas' guide, I built a YUI Test Suite around the first method, _uriLink:

        new YAHOO.tool.TestCase({
            name: "_uriLink Tests",
        
            // Setup and Tear Down
            setUp: function () {
                this.uri1 = "http://blog.foxxtrot.net/";
                this.uri1_expected = '' + this.uri1 + '';
                this.uri2 = "http://search.yahoo.com/search?p=yui&ei=UTF-8&fr=moz2";
                this.uri2_expected = '' + this.uri2 + '';
                this.message1 = "Test Message";
                this.message2 = "Second Test Message";
            },
            tearDown: function () {
                delete this.uri1;
                delete this.uri1_expected;
                delete this.uri2;
                delete this.uri2_expected;
                delete this.message1;
                delete this.message2;
            },
        
            // Tests
            testSimple: function () {
                YAHOO.util.Assert.areEqual(this.uri1_expected, fwl._uriLink(this.uri1));
            },
            testBegining: function () {
                var message = this.message1 + ' ' + this.uri1;
                var expected = this.message1 + ' ' + this.uri1_expected;
                YAHOO.util.Assert.areEqual(expected, fwl._uriLink(message));
            },
            testEnd: function () {
                var message = this.uri1 + ' ' + this.message1;
                var expected = this.uri1_expected + ' ' + this.message1;
                YAHOO.util.Assert.areEqual(expected, fwl._uriLink(message));
            },
            testMiddle: function () {
                var message = this.message1 + ' ' + this.uri1 + ' ' + this.message2;
                var expected = this.message1 + ' ' + this.uri1_expected + ' ' + this.message2;
                YAHOO.util.Assert.areEqual(expected, fwl._uriLink(message));
            },
            testMultiple_Same: function () {
                var message = this.message1 + ' ' + this.uri1 + ' ' + this.uri1 + ' ' + this.message2;
                var expected = this.message1 + ' ' + this.uri1_expected + ' ' + this.uri1_expected + ' '  + this.message2;
                YAHOO.util.Assert.areEqual(expected, fwl._uriLink(message));
            },
            testMultiple_Different: function () {
                var message = this.message1 + ' ' + this.uri1 + ' ' + this.uri2 + ' ' + this.message2;
                var expected = this.message1 + ' ' + this.uri1_expected + ' ' + this.uri2_expected + ' '  + this.message2;
                YAHOO.util.Assert.areEqual(expected, fwl._uriLink(message));
            }
        })
        

This took me maybe twenty minutes to finish. It was easy, and the code is very clear. But it wasn't all roses. It turns out, my code has a bug.

YUITestFail.png

Yep, if a posting appears with the same URL more than once, the code fails. Now, I don't really expect that to happen, but it's a problem if someone links a site, and a subpage of the URL, so I'll need to fix it. Again, not likely in 140 characters, but even so it is a bug, and it's not completely outside the realm of possibility, so I'll be needing to fix it.

I really like YUI Test. It's a powerful framework, being able to do both traditional unit testing, like I do above, but also behavioral testing where you can simulate events and test for actions. It's the only one I've seen which tries to do both things, and does them both well. Unit Testing is one of those things that I see a lot of value in, though I've not completely jumped down that path for anything I've written, just yet.

Still, what I feel I want or need to see is simple. I want to be able to unit test private methods. I want to be able to perform my unit tests without firing up a full browser. The only way I can see to accomplish this, is going to be with a command-line JavaScript engine. Luckily, you can get SpiderMonkey, the Mozilla JavaScript Engine, available on the command-line, which I already use to make sure my code is at least parseable. Some of what I want to do, I can probably do just by writing mock objects in JavaScript, some, I may need to modify a JavaScript interpreter to allow access to 'private' methods.

While I may be complaining a bit about some of the specifics of Unit Testing JavaScript, the fact is that the tools needed to do it are available today, and if you work on Web Applications at all, you really should start unit testing all of your code, JavaScript isn't an impossible case anymore.