开发者

Test-driven development of JavaScript web frontends

This might sound a little dumb, but I'm actually a bit confused how to approach JavaScript testing for web frontends. As far as I'm concerned, the typical 3-tier architecture looks like this:

  1. Database tier
  2. Application tier
  3. Client tier

1 is of no concern in this question. 2 contains all the program logic ("business logic") 3 the frontend.

I do test-driven development for most projects, but only for the application logic, not the frontend. That is because testing the UI is difficult and unusual in TDD, and normally not done. Instead, all application logic is separated from UI, so that it is simple to test that logic.

The three tier architecture supports this: I can design my backend as a REST API which is called by my frontend. How does JS testing fit in? For the t开发者_C百科ypical three-tier-architecture, JS (i.e. JS on the client) testing doesn't make much sense, does it?

Update: I've changed the question's wording from "Testing JavaScript in web frontends" to "Test-driven development of JavaScript web frontends" to clarify my question.


Remember what the point of unit-testing is: to ensure a particular module of code reacts to some stimuli in an expected manner. In JS, a significant portion of your code, (unless you have some lifecycle framework like Sencha or YUI) will either be directly manipulating the DOM or making remote calls. To test these things, you simply apply traditional unit-testing techniques of dependency injection and mocking/stubbing. That means you must write each function, or class, that you want to unit-test to accept mocks of the dependent structures.

jQuery supports this by allowing you to pass an XML document into all traversal functions. Whereas you might normally write

$(function() { $('.bright').css('color','yellow'); }

you'll instead want to write

function processBright(scope) {
    // jQuery will do the following line automatically, but for sake of clarity:
    scope = scope || window.document;

    $('.bright',scope).css('color','yellow');
}

$(processBright);

Notice we not only pull the logic out of the anonymous function and give it a name, we also make that function accept a scope parameter. When that value is null, the jQuery calls will still function as normal. However, we now have a vector for injecting a mock document that we can inspect after the function is invoked. The unit-test could look like

function shouldSetColorYellowIfClassBright() {
    // arrange
    var testDoc = 
        $('<html><body><span id="a" class="bright">test</span></body></html>');

    // act
    processBright(testDoc);

    // assert
    if (testDoc.find('#a').css('color') != 'bright')
        throw TestFailed("Color property was not changed correctly.");
}

TestFailed could look like this:

function TestFailed(message) {
    this.message = message;
    this.name = "TestFailed";
}

The situation is similar with remote calls, though rather than actually injecting some facility, you could get away with a masking stub. Say you have this function:

function makeRemoteCall(data, callback) {
    if (data.property == 'ok') 
        $.getJSON({url:'/someResource.json',callback:callback});
}

You would test it as such:

// test suite setup
var getJSON = $.getJSON;
var stubCalls = [];
$.getJSON = function(args) {
    stubCalls[stubCalls.length] = args.url;
}

// unit test 1
function shouldMakeRemoteCallWithOkProperty() {
    // arrange
    var arg = { property: 'ok' };

    // act
    makeRemoteCall(arg);

    // assert
    if (stubCalls.length != 1 || stubCalls[0] != '/someResource.json')
        throw TestFailed("someResource.json was not requested once and only once.");
}

// unit test 2
function shouldNotMakeRemoteCallWithoutOkProperty() {
    // arrange
    var arg = { property: 'foobar' };

    // act
    makeRemoteCall(arg);

    // assert
    if (stubCalls.length != 0)
        throw TestFailed(stubCalls[0] + " was called unexpectedly.");
}

// test suite teardown
$.getJSON = getJSON;

(You can wrap that whole thing in the module pattern to not litter the global namespace.)

To apply all of this in a test-driven manner, you would simply write these tests first. This is a straightforward, no frills, and most importantly, effective way of unit-testing JS.

Frameworks like qUnit can be used to drive your unit-tests, but that is only a small part of the problem. Your code must be written in a test-friendly way. Also, frameworks like Selenium, HtmlUnit, jsTestDriver or Watir/N are for integration testing, not for unit-testing per se. Lastly, by no means must your code be object-oriented. The principles of unit-testing are easily confused with the practical application of unit-testing in object-oriented systems. They are separate but compatible ideas.

Testing Styles

I should note that two different testing styles are demonstrated here. The first assumes complete ignorance of the implementation of processBright. It could be using jQuery to add the color style, or it could be doing native DOM manipulation. I'm merely testing that the external behavior of the function is as expected. In the second, I assume knowledge of an internal dependency of the function (namely $.getJSON), and those tests cover the correct interaction with that dependency.

The approach you take depends on your testing philosophy and overall priorities and cost-benefit profile of your situation. The first test is relatively pure. The second test is simple but relatively fragile; if I change the implementation of makeRemoteCall, the test will break. Preferably, the assumption that makeRemoteCall uses $.getJSON is at least justified by the documentation of makeRemoteCall. There are a couple more disciplined approaches, but one cost-effective approach is to wrap dependencies in wrapper functions. The codebase would depend only on these wrappers, whose implementations can be easily replaced with test stubs at test-time.


There is a book titled Test-Driven JavaScript Development by Christian Johansen that might help you. I have only looked at some of the samples in the book (just downloaded a sample to Kindle the other day) but it looks like a great book that addresses this very issue. You might check it out.

(Note: I have no connection with Christian Johansen and no investment in sales of the book. Just looks like a good thing that addresses this problem.)


I have a similary architected application with JS client tier. In my case i use our company's own JS-framework to implement client tier.

This JS framework is created in OOP-style thus i can implement unit-testing for core classes and components. Also, to cover all user interactions (which can't be covered using unit-testing) i am using Selenium WebDriver to do an integration testing of framework visual components and test them under different browsers.

So, TDD can be applied to JavaScript development if code under test is written in OOP-manner. Also integration test is also possible (and can be used to do some kind of TDD).


Have a look at QUnit, as well, for unit tests of JavaScript methods and functions.


You can test your application from a user perspective with tools such as Rational Functional Tester, the HP tools or other equivalent software.

These tools test the application as if a user was sitting in front of it, but in an automated fashion. This means that you can test all three tiers at the same time, and especially the Javascript which may be difficult to test otherwise. Functional testing like this may help to find UI bugs and quirks with how the UI is using the data pushed out by your middle tier.

Unfortunately these tools are very expensive, so there may be other equivalents (and I'd be interested to know of such tools).


In our company we use jsTestDriver. It's a feature rich environment for testing frontend.

Take a look at it.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜