Archive

Posts Tagged ‘yuitest’

Testable Web UI VII: Postback

February 19th, 2009
Comments Off

Last time, we got a bit derailed by some issues that were getting in the way of effective tests. Now that that’s all wrapped up, let’s implement selecting equipment.

The primary requirement here is that we have to display the player’s equipment, and when they select an item by clicking on it, we post to the server to save the player’s selection.

First, I populate the inventory list with the result of another JSON service. This is nothing new, really, so I will skip the details. However, I will need the __renderInventory function for the next step:

__renderInventory : function(inventory) {
    var n = this.__getChild('inventory');
    while (n.firstChild) {
        n.removeChild(n.firstChild);
    }

    for (var index in inventory) {
        var i = inventory[index];
        var li = document.createElement('li');
        li.setAttribute('class', 'inventory-item item-' + index);
        li.innerHTML = i.name;
        li.onclick = function(self, index) {
            self.__equipItem(index);
        }.bind(self, index);
        n.appendChild(li);
    }
},

__equipItem : function(index) {
    // TODO!
},

To write this test, I expanded FakeNetwork a bit, to encompass the ability to make POST requests as well as GET:

function FakeNetwork() {
    this.requests = {};
}

FakeNetwork.prototype = {
    asyncRequestJson : function(url, onComplete) {
        this.requests[url] = [null, onComplete];
    },

    post : function(url, data, onComplete) {
        this.requests[url] = [data, onComplete];
    },

    isRequestPending : function(url) {
        return url in this.requests;
    },

    getRequestData : function(url) {
        Assert.isTrue(this.isRequestPending(url), 'FakeNetwork: "' + url + '" has no data, as it is not pending');
        return this.requests[url][0];
    },

    completeRequest : function(url, result) {
        Assert.isTrue(this.isRequestPending(url), 'FakeNetwork: "' + url + '" is not pending.');
        this.requests[url][1](result);
        delete this.requests[url];
    },
};

This allows me to write the test:

testClickInventoryPostsToEquipService : function() {
    var sw = new StatusWidget('avatar-1', this.net);
    YAHOO.util.UserAction.click(getNode('avatar-1', 'equipment'));

    this.net.completeRequest('getInventory.json', [
        {name:'Clown Shoes', kickboxing: 9001}
    ]);

    Assert.isFalse(this.net.isRequestPending('updateEquipment.php'));
    YAHOO.util.UserAction.click(getNode('avatar-1', 'inventory-item item-0'));

    Assert.isTrue(this.net.isRequestPending('updateEquipment.php'), 'Service not dispatched');
    Assert.areEqual('Clown Shoes', this.net.getRequestData('updateEquipment.php').name);
},

The implementation is so simple as to be boring:

__equipItem : function(index) {
    this.net.post('updateEquipment.php', {name:this.inventory[index].name});
},

The last gap that is missing is that our real Network object can’t POST yet, but that is not sufficiently exciting to post here. (also, I don’t know how to write a functional test without writing some kind of CGI service, so I am going to cop out :D)

At this point, we’ve covered just about all of the major sticking points: introspecting CSS and the DOM at runtime, test isolation, asynchronous logic, mocking the network, and, of course, tests that are both reliable and really bloody fast: I ended with a pretty modest test suite of 7 tests that take (on my machine) 381ms to run. I’m about to call the experiment a raging success.

Tomorrow, I will talk about… something. It will be a surprise to everyone!

Source code

andy Uncategorized ,

Testable Web UI VI: The Evils of Shared State

February 18th, 2009
Comments Off

So, last time, we got to showing an inventory panel when clicking a menu item. Since I cheated last time and wrote the code to hide without a test, let’s start off by fixing our mistake:

testClickEquipmentTwiceHidesInventory : function() {
    YAHOO.util.UserAction.click(getNode("avatar-1", "equipment"));
    YAHOO.util.UserAction.click(getNode("avatar-1", "equipment"));

    Assert.areEqual(
        'none',
        getNode("avatar-1", "inventory").style["display"]
    );
}

Easy, right?

Wrong.

As written, this test fails because the initial state of the display is not what we expect it to be. In fact, we can’t rely on its state being anything because YUI Test explicitly states that it will run tests in whatever order it darn well pleases, and we’ve yet to do anything at all to try to clean up after ourselves.

We’ve come a fairly long way without bumping into this, but this always happens whenever your tests have some kind of external dependancy. Sooner or later, they start tromping all over each other, and you have to figure out what to do.

We have a few options:

  1. Make StatusWidget itself responsible for generating the HTML. This wouldn’t be bad from a design standpoint, but it means that the user might see an incomplete page if page loading takes too long.
  2. Write a setUp() function to initialize the state of our HTML to something that we expect. This isn’t quite as robust as destroying and recreating it every test run, but it will be fast and easy. The only catch is that we will have to keep changing our setup every time we add more to the markup.
  3. Wire the test setUp() to completely regenerate the HTML before every test is run. This is pretty good, as it doesn’t force us to make any design treadoffs that might not pan out, and it will be robust in the face of changes to our widget.

Generally speaking, you can usually assume that the scorched earth approach to setting up your testing data will be the right way. I’ll demonstrate why in a moment.

This, as it happens, is pretty easy. I’ll just put the HTML in a hidden ‘template’ div, and copy it before every test run:

setUp : function() {
    var t = document.getElementById('avatar-template');
    document.getElementById('avatar-1').innerHTML = t.innerHTML;
},

We also need to make a tiny change to the markup:

<div id="avatar-1" class="avatar"></div>

<div style="display:none">
    <div id="avatar-template" class="avatar">
        <div class="name">andy</div>
        <img class="avpic" src="avatar.png" />
        <div class="details">
            <div class="hitpoints_panel">HP:
                <span class="hit_point_score">5</span>
            </div>
            <div class="kickboxing_panel">Kickboxing:
                <span class="kickboxing_score">6</span>
            </div>
            <div class="linear_algebra_panel">Linear Algebra:
                <span class="linear_algebra_score">7</span>
            </div>
            <div class="cross_stitching_panel">Cross-stitching:
                <span class="cross_stitching_score">8</span>
            </div>
        </div>
        <div class="main_menu">
            <div class="equipment">Equipment</div>
        </div>
        <ul class="inventory"><li>Filler</li></ul>
    </div>
</div>

This still fails, though, and when I click the Equipment button manually, it fails to do anything. What gives?

This does:

testClickEquipmentExpandsInventory : function() {
    // Conspicuous lack of: var sw = new StatusWidget('avatar-1');
    YAHOO.util.UserAction.click(getNode("avatar-1", "equipment"));

    Assert.areEqual(
        'block',
        getNode("avatar-1", "inventory").style["display"]
    );
},

As you can see, test dependancies are insidious, and, if we hadn’t pulled out the big hammer and blown the DOM away before every test, we would never have noticed this particular bug.

Tomorrow, we’ll get to populating the inventory.

Source code

andy Uncategorized ,

Testable Web UI V: Clicky!

February 17th, 2009
Comments Off

Last time, I started diving into using a JSON service to pull statistics into an HTML widget. I stopped right after that point because I thought, at the time, that it was worth spending a moment to think about what the target design should be.

After spending a few minutes staring at the code, though, I think I’m okay: StatusWidget is fully complaint with the SRP, and whatever other rules I can think of. So, instead of pontificating on what separates a serviceable design from a superior design, I am instead going to code more things!

Let’s go back to the initial set of requirements:

  • I want to see my character’s picture, name, and statistics
  • I want to be able to pick some equipment for my character
  • I want to see how my character’s equipment affects its statistics

The first one seems to work fine, so let’s move on to selecting equipment. Time to start coding some interactivity!

What I’ll be doing next is a sort of ‘drawer’ interface: A “Show Inventory” button that, when clicked, will toggle between displaying and hiding an inventory view (that we also have to code)

The current pattern I have been running with is to just dump markup into the test file, which is probably not sustainable (TODO: figure this out), but for now, it seems to be scaling pretty well. Let’s throw a menu in:

<ul class="main_menu">
    <li class="equipment">Equipment</li>
</ul>
<ul class="inventory"><li>Filler</li></ul>

It doesn’t look like much, but we’ve just introduced another dependancy: we want the inventory to be hidden most of the time, so we need some CSS to do this:

.avatar > .inventory {
    display: none;
}

There. Now we can’t see the filler.

That was fun. Now let’s make the button toggle the visibility.

testClickEquipmentExpandsInventory : function() {
    YAHOO.util.UserAction.click(getNode("avatar-1", "equipment"));

    Assert.areEqual(
        'block',
        getNode("avatar-1", "inventory").style["display"]
    );
}

I spent almost an hour trying to figure out how to test whether the element was visible, and I have a feeling that even this is not quite airtight. (I will ask one of my HTML god friends about this later) Nevertheless, I get a red bar, so I am going to forge ahead!

Now to make my hard-earned test pass:

function StatusWidget() {
    /* stuff we've already seen before */

    var self = this;
    this.__getChild("equipment").onclick = function() {
        self.toggleInventoryDisplay();
    };
}

/* ... */
StatusWidget.prototype = {
    toggleInventoryDisplay : function() {
        var n = this.__getChild("inventory");
        if (n.style['display'] == 'block') {
            n.style['display'] = 'none';
        } else {
            n.style['display'] = 'block';
        }
    },

    /* stuff */
}

This test will consistently pass, but it suffers from a subtle, but very common problem that will wreck testing later. Identifying it is left as an exercise to the reader. (HINT: it has nothing to do with my terrible non-browser-compatible JS, or my pisspoor CSS. It is specifically a testability bug)

Tomorrow, I will fix it and (time permitting) populate the inventory list.

Source code

andy Uncategorized ,

Testable Web UI IV: YUI Test (plus a bit of mocking)

February 16th, 2009
Comments Off

So, last time, I was trying to write a test around requesting some JSON data from a service, and return it. Unfortunately, JsUnit is not up to the task of handling asynchronous logic at all, so I decided to ditch it in search of greener pastures.

Having spent some time working with YUI in the past, I settled on trying YUI Test out.

It took some finagling, but I successfully managed to convert my first test over to the new testing framework. It’s got quite a lot of boilerplate, so I won’t inline it, but you can see it here.

Now that we’ve regained lost ground, time to try what we were doing before. Here’s my test:

testCanRequestJson : function() {
    var self = this;
    asyncRequestJson('getStats.json', function(result) {
        self.resume(function() {
            Assert.areEqual('andy', result.name);
            Assert.areEqual(9999, result.hit_points);
            Assert.areEqual(1, result.kickboxing);
            Assert.areEqual(99, result.linear_algebra);
            Assert.areEqual(99, result.cross_stitching);
        });
    });

    self.wait();
},

The way it works is that the “self.wait();” call tells the testing framework to wait until a corresponding “self.resume(fn).” If the resume doesn’t come within a few seconds, the test fails. It’s a bit ugly, but it does the job admirably, and I don’t know how you’d do it better without fibres or coroutines or something, so I’m not about to complain too loudly.

This is great, as it demonstrates I can interact with the network in a sensible way, but it’s kind of annoying that I have to have an extra data file just to make the test run. I think it’s fine for a functional test like this, but I don’t want to have to write functional tests for everything.

The next step is to plug these values into my widget. Here’s my test:

testWidgetIsPopulated : function() {
    var grueStats = {
        name:"Grue",
        hit_points:1,
        kickboxing:1,
        linear_algebra:0,
        cross_stitching:99999
    };

    var net = new FakeNetwork();
    var sw = new StatusWidget('avatar-1',
        function (url, onComplete) { return net.asyncRequestJson(url, onComplete); }
    );

    net.completeRequest('getStats.json', grueStats);

    Assert.areEqual("Grue", sw.getName());
    Assert.areEqual(1, sw.getHitPoints());
    Assert.areEqual(1, sw.getKickboxingScore());
}

FakeNetwork? Whassat? It’s this:

function FakeNetwork() {
    this.requests = {};
}

FakeNetwork.prototype = {
    asyncRequestJson : function(url, onComplete) {
        this.requests[url] = onComplete;
    },

    completeRequest : function(url, result) {
        this.requests[url](result);
        delete this.requests[url];
    }
};

Simple, yes? This way, I don’t have to actually have a JSON service for every little thing, and I don’t have to worry about asynchronous results or network funkiness. Win!

Tomorrow, I will do some badly-needed refactoring, and lay out what the future direction of the design should be.

Source code

andy Uncategorized ,