Diff for "JavascriptUnitTesting"

Not logged in - Log In / Register

Differences between revisions 21 and 22
Revision 21 as of 2012-09-17 18:13:49
Size: 7680
Editor: abentley
Comment: Fix HTML template.
Revision 22 as of 2012-09-17 18:57:49
Size: 5622
Editor: rharding
Comment:
Deletions are marked like this. Additions are marked like this.
Line 41: Line 41:
    * It can test for failure, but Exception testing in JavaScript requires some extra work.
Line 53: Line 52:
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.
JS testing involves two parts. One is an html file that will bootstrap the
environement and load the required code needed to test. There are sample
templates to help bootstrap your tests in the application root.
Line 58: Line 56:
{{{
<html>
  <head>
    <title>Launchpad TESTLIBRARY</title>
    <!-- YUI and test setup -->
    <script type="text/javascript"
            src="../../../../../build/js/yui/yui/yui.js">
    </script>
    <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
    <script type="text/javascript"
            src="../../../../app/javascript/testing/testrunner.js"></script>
Copy them from:
Line 70: Line 58:
    <!-- The module under test -->
    <script type="text/javascript" src="../TESTLIBRARY.js"></script>
  * standard_test_template.html
  * standard_test_template.js
Line 73: Line 61:
    <!-- The test suite -->
    <script type="text/javascript" src="test_TESTLIBRARY.js"></script>
  </head>
  <body class="yui3-skin-sam">
The first step is to setup the html file. You'll need to adjust the paths of
the various script files included. All code not under direct test should be
pulled from the build/js directory. Replace ${LIBRARY} with the name of your
module you're testing.
Line 78: Line 66:
    <!-- The example markup required by the script to run -->
    <div id="expected-id">
      ...
    </div>
  </body>
</html>
}}}
For instance if you're working on adding tests to app.widget you'd:
Line 86: Line 68:
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.
    %s/${LIBRARY}/app.widget
Line 91: Line 70:
If you write a bad test setup, you will break the runner, it will never
complete.
The test runner picks up the modules that need to be run based on the <li>
items in <ul id="suites">. If you did the replace above you'll find that the
test module is setup for you.
Line 94: Line 74:
    <li>lp.app.widget.test</li>
Line 95: Line 76:
=== The Test module === The second step is to prep the JS file that runs the tests themselves. The
demo files is setup for the same find and replace of ${LIBRARY}. It will
complete the name of the module for you. It's setup to add a .test to the end
of the module you're testing.
Line 97: Line 81:
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().use('lp.testing.runner', 'test', '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();
            },
        }));

    Y.lp.testing.Runner.run(suite);
});
}}}

'''Note''' that the tests are run as part of our test suite. For that reason, the{{{fetchCSS: false}}} setting is important as it prevents the test from downloading CSS from an external source. However, in development and debugging you'll want to change that to {{{fetchCSS: true}}} so that the output is well formatted. Just be sure you don't check in the file with it set to true.
The default template has an initial test that will check that it can load the
module you're looking to test. After that find and replace, you should be able
to open the html file and get a single passing test. If you get any errors,
correct them now before moving on to add more tests. Potential errors include
missing dependency modules, typos in the names of the module, or the paths to
the build directory being off.
Line 161: Line 96:
The YUI unit tests can be run from the command line but only at a per-file granularity.  Specify a pattern that matches the html file, e.g. The YUI unit tests can be run from the command line.
Line 163: Line 98:
bin/test -vv -t test_buglisting.html xvfb-run ./bin/test -x -cvv --layer=YUITestLayer

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/lp/registry/javascript.

What is YUI.Test

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

It is not as solid as other xUnit frameworks.

  • JavaScript does not support imports which can be used to provide stubs and mocks. The Y.Mock tools can test params and methods, but not callables; you will need to monkey patch a callable when you need to verify that your script passed the expected arguments to a YUI method.

  • It is automated, but the best way to run tests interactively is in your browser.

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

JS testing involves two parts. One is an html file that will bootstrap the environement and load the required code needed to test. There are sample templates to help bootstrap your tests in the application root.

Copy them from:

  • standard_test_template.html
  • standard_test_template.js

The first step is to setup the html file. You'll need to adjust the paths of the various script files included. All code not under direct test should be pulled from the build/js directory. Replace ${LIBRARY} with the name of your module you're testing.

For instance if you're working on adding tests to app.widget you'd:

  • %s/${LIBRARY}/app.widget

The test runner picks up the modules that need to be run based on the <li> items in <ul id="suites">. If you did the replace above you'll find that the test module is setup for you.

  • <li>lp.app.widget.test</li>

The second step is to prep the JS file that runs the tests themselves. The demo files is setup for the same find and replace of ${LIBRARY}. It will complete the name of the module for you. It's setup to add a .test to the end of the module you're testing.

The default template has an initial test that will check that it can load the module you're looking to test. After that find and replace, you should be able to open the html file and get a single passing test. If you get any errors, correct them now before moving on to add more tests. Potential errors include missing dependency modules, typos in the names of the module, or the paths to the build directory being off.

Running Tests

In a browser

To run the tests interactively simply load the html file into your browser. A successful run, with fetchCSS:true, looks like: yui-test.png

Command line

The YUI unit tests can be run from the command line.

xvfb-run ./bin/test -x -cvv --layer=YUITestLayer

Unfortunately you may not run a single test.

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.

Mocking XHR Calls

Tests will often have a requirement that you want to see that the DOM is correctly updated as a result of a user initiated action such as a button click. Sometimes, in production, such a user action may result in an XHR call where the response data is used to update the DOM. In such cases, you still want to be able to test the interaction within a YUI test without having to resort to using an integration test. To make this easy we have MockIo class in Launchpad.

Notes

[1] Heisenkoans

  • Does a test pass if it is never run?
    Is a test written if nothing is ever tested with it?


CategoryTesting CategoryJavaScript

JavascriptUnitTesting (last edited 2021-10-06 13:24:38 by cjwatson)