Home > Uncategorized > Testable Web UI VI: The Evils of Shared State

Testable Web UI VI: The Evils of Shared State

February 18th, 2009

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

Uncategorized ,

Comments are closed.