JavascriptUnitTesting

Not logged in - Log In / Register

Revision 2 as of 2010-01-18 15:48:03

Clear message

Developing with YUI.Test

Or putting the fun back into JavaScript

This is a short primer to introduce you to YUI.Test. As you might have noticed, hacking in JavaScript, né LiveScript, ceased to be fun when Internet Explore implemented JScript, a freak cross bread of the JavaScript syntax and VBScript. Other browser makers entered into the cross-breading competition and I left for a job that let me put all the logic in the server.

I spent two days creating the milestone_table script that allows users to create milestones and add them to the table. This involved a refactoring of the milestoneoverlay script and adding what appeared to be a few lines to call the existing view that renders a milestone table row.

Day one was horrible, there was a lot of shouting, words were said that may have hurt my computer's feelings. There were two problems. The first was that my extraction of the milestoneoverlay script had failed. This was not true as I learned. The Windmill test broke weeks before I started my branch because the page layout changed [1]. The second problem was the complexity of corner-cases led to unpredictable behaviors.

Day two was a fun roller coaster ride refactoring my script from first principles using unit tests. YUI.Test made me write a library, structure my code to have simple contracts to do simple things, and let me refactor safely. After a few hours, I was able to bring predictable behaviour to the script and I believe the code was much easier to read.

You can see the milestone_table script library and its tests at /lib/canonical/launchpad/javascript/registry

What is YUI.Test

You can read the summary and API of YUI.Test at

It is not as solid as other xUnit frameworks.

It is better than anything else we have used. It is fast to develop, easy to maintain, and allows good designs to emerge. Since the test harness is a page, you can develop a library/widget in many small branches before you decide it is ready to include in the zpt.

...................... The Harness and Runner ......................

This is a rudimentary harness. You need to replace TESTLIBRARY with the name of the library you are writing. Your library probably requires some markup to run, what it connects to and modifies. Add that markup with ids and classes too.

<html>
  <head>
    <title>Launchpad TESTLIBRARY</title>
    <!-- YUI 3.0 Setup -->
    <script type="text/javascript" src="../../../icing/yui/current/build/yui/yui.js"></script>
    <script type="text/javascript" src="../../../icing/lazr/build/lazr.js"></script>
    <link rel="stylesheet" href="../../../icing/yui/current/build/cssreset/reset.css"/>
    <link rel="stylesheet" href="../../../icing/yui/current/build/cssfonts/fonts.css"/>
    <link rel="stylesheet" href="../../../icing/yui/current/build/cssbase/base.css"/>
    <link rel="stylesheet" href="../../test.css" />

    <!-- The module under test -->
    <script type="text/javascript" src="../TESTLIBRARY.js"></script>

    <!-- The test suite -->
    <script type="text/javascript" src="test_TESTLIBRARY.js"></script>
  </head>
  <body class="yui-skin-sam">

    <!-- The example markup required by the script to run -->
    <div is="expected-id">
      ...
    </div>
  
    <!-- The test output -->
    <div id="log"></div>
  </body>
</html>

Load that page into your browser. The tests will be run. If you get a js error on the page, the script tags or example markup need fixing. Reload your browser to replay the tests. The output could use some CSS love to clarify subordinate and summary information.

If you write a bad test setup, you will break the runner, it will never complete.

The Test module

This is a rudimentary test suite. Replace mynamespace with your library's namespace to get started. The interesting block is the _should property, which defines the tests that should raise an error.

YUI({
    base: '../../../icing/yui/current/build/',
    filter: 'raw', combine: false
    }).use('yuitest', 'console', 'lp.mynamespace', function(Y) {

    var mynamespace = Y.lp.mynamespace;
    var suite = new Y.Test.Suite("mynamespace Tests");

    suite.add(new Y.Test.Case({
        // Test the setup method.
        name: 'setup',

        _should: {
            error: {
                test_config_undefined: true,
                }
            },

        setUp: function() {
            this.tbody = Y.get('#milestone-rows');
            },

        tearDown: function() {
            delete this.tbody;
            mynamespace._milestone_row_uri_template = null;
            mynamespace._tbody = null;
            },

        test_good_config: function() {
            // Verify the config data is stored.
            var config = {
                milestone_row_uri_template: '/uri',
                milestone_rows_id:  '#milestone-rows'
                };
            mynamespace.setup(config);
            Y.Assert.areSame(
                config.milestone_row_uri_template,
                mynamespace._milestone_row_uri_template);
            Y.Assert.areSame(this.tbody, mynamespace._tbody);
            },

        test_config_undefined: function() {
            // Verify an error is thrown if there is no config.
            mynamespace.setup();
            },
        }));

    // Lock, stock, and two smoking barrels.
    Y.Test.Runner.add(suite);

    var console = new Y.Console({newestOnTop: false});
    console.render('#log');

    Y.on('domready', function() {
        Y.Test.Runner.run();
        });
});

Considerations for Tests

Use the get/set wrappers to access innerHTML to reset the HTML DOM for a test. clone was not reliable as I thought it would be.

The Y.Node.create() methods expects a single root node in the raw markup. I had to strip whitespace to ensure the node was created in my library, so I tested that in the library tests.

I had to monkey patch Y.io to verify it was called with the correct parameters. The YUI guide suggests that I refactor the get_milestone_row to accept a Y.io as parameter so that it is easy to pass a mock. I decided that I did not want test to determine the public API of method.

Another reason I monkey patched Y.io is that it is an async call. The test always completes before the return. As above I could have added a test parameter to the public method to set the timeout to 0001 milliseconds. I decided that Y.IO is not under test, so the best test would not use it.

Notes

[1] Heisenkoans


CategoryTesting