Skip to main content

The Fundamentals of a JavaScript Test Suite

Posted by manning_pubs on February 7, 2013 at 5:18 AM PST



The Fundamentals of a JavaScript Test Suite

by John Resig and Bear Bibeault, authors of Secrets of the JavaScript Ninja

As important as a solid testing strategy is for all code, it can be crucial for situations where external factors have the potential to affect the operation of your code, which is exactly the case you're faced with in cross-browser JavaScript development. In this article based on chapter 2 of Secrets of the JavaScript Ninja, the authors show you how to construct a test suite to reliably run those tests.

The primary purpose of a test suite is to aggregate all the individual tests that your code base might have into a single location, so that they can be run in bulk, providing a single resource that can be run easily and repeatedly.

To better understand how a test suite works, it makes sense to look at how a test suite is constructed. Perhaps surprisingly, JavaScript test suites are really easy to construct. A functional one can be built in only about 40 lines of code.

You would have to ask, though, "Why would I want to build a new test suite?" For most cases, it probably isn't necessary to write your own JavaScript test suite. There are already a number of good-quality suites to choose from (as already shown). But building your own test suite can serve as a good learning experience, especially when looking at how asynchronous testing works.

The assertion

The core of a unit-testing framework is its assertion method, usually named assert(). This method usually takes a value—n expression whose premise is asserted—and a description that describes the purpose of the assertion. If the value evaluates to true and, in other words, is "truthy," the assertion passes; otherwise, it's considered a failure. The associated message is usually

logged with an appropriate pass/fail indicator.

A simple implementation of this concept can be seen in the next listing.

Listing 1 A simple implementation of a JavaScript assertion

<html>
  <head>
    <title>Test Suite</title>
    <script>

      function assert(value, desc) { 

        var li = document.createElement("li");

        li.className = value ? "pass" : "fail";

        li.appendChild(document.createTextNode(desc));

        document.getElementById("results").appendChild(li);

      } 


      window.onload = function() {
        assert(true, "The test suite is running.");

        assert(false, "Fail!"); 

      };
    </script>
   
    <style>
      #results li.pass { color: green; } 

      #results li.fail { color: red; } 

    </style>
  </head>

  <body>
    <ul id="results"></ul>

  </body>
</html>

The function named assert() (#1) is almost surprisingly straightforward. It creates a new

  • element containing the description, assigns a class named pass or fail, depending upon the value of the assertion parameter (value), and appends the new element to a list element in the document body (#4).

    The test suite consists of two trivial tests (#2): one that will always succeed, and one that will always fail.

    Style rules for the pass and fail classes (#3) visually indicate success or failure using colors.

    This function is simple, but it will serve as a good building block for future development.

    Test groups

    Simple assertions are useful, but they really begin to shine when they're grouped together in a testing context to form test groups.

    When performing unit testing, a test group will likely represent a collection of assertions as they relate to a single method in our API or application. If you were doing behavior-driven development, the group would collect assertions by task. Either way, the implementation is effectively the same.

    In our sample test suite, a test group in which individual assertions are inserted into the results is built. Additionally, if any assertion fails, then the entire test group is marked as failing. The output in the next listing is kept pretty simple—some level of dynamic control would prove to be quite useful in practice (contracting/expanding the test groups and filtering test groups if they have failing tests in them).

    Listing 2 An implementation of test grouping

    <html>
      <head>
        <title>Test Suite</title>
        <script>

          (function() {
            var results;
            this.assert = function assert(value, desc) {
              var li = document.createElement("li");
              li.className = value ? "pass" : "fail";
              li.appendChild(document.createTextNode(desc));
              results.appendChild(li);
              if (!value) {
                li.parentNode.parentNode.className = "fail";
              }
              return li;
            };
            this.test = function test(name, fn) {
              results = document.getElementById("results");
              results = assert(true, name).appendChild(
                  document.createElement("ul"));
              fn();
            };
          })();

          window.onload = function() {
            test("A test.", function() {
              assert(true, "First assertion completed");
              assert(true, "Second assertion completed");
              assert(true, "Third assertion completed");
            });
            test("Another test.", function() {
              assert(true, "First test completed");
              assert(false, "Second test failed");
              assert(true, "Third assertion completed");
            });
            test("A third test.", function() {
              assert(null, "fail");
              assert(5, "pass")
            });
          };
        </script>
        <style>
          #results li.pass { color: green; }
          #results li.fail { color: red; }
        </style>
      </head>
      <body>
        <ul id="results"></ul>
      </body>
    </html>

    As seen in listing 2, the implementation is really not much different from the basic assertion logging. The one major difference is the inclusion of a results variable, which holds a reference to the current test group (that way the logging assertions are inserted correctly).

    Beyond simple testing of code, another important aspect of a testing framework is the handling of asynchronous operations.

    Asynchronous testing

    A daunting and complicated task that many developers encounter while developing a JavaScript test suite is handling asynchronous tests. These are tests whose results will come back after a nondeterministic amount of time has passed; common examples of this situation could be Ajax requests or animations.

    Often the handling of this issue is over-engineered and made much more complicated than it needs to be. To handle asynchronous tests, we need to follow a couple of simple steps:

    1. Assertions that rely upon the same asynchronous operation need to be grouped into a unifying test group.
    2. Each test group needs to be placed on a queue to be run after all the previous test groups have finished running.

    Thus, each test group must be capable of running asynchronously.

    Let's look at an example in the next listing.

    Listing 3 A simple asynchronous test suite

    <html>
      <head>
        <title>Test Suite</title>
        <script>
          (function() {
            var queue = [], paused = false, results;
            this.test = function(name, fn) {
              queue.push(function() {
                results = document.getElementById("results");
                results = assert(true, name).appendChild(
                    document.createElement("ul"));
                fn();
              });
              runTest();
            };
            this.pause = function() {
              paused = true;
            };
            this.resume = function() {
              paused = false;
              setTimeout(runTest, 1);
            };
            function runTest() {
              if (!paused && queue.length) {
                queue.shift()();
                if (!paused) {
                  resume();
                }
              }
            }

            this.assert = function assert(value, desc) {
              var li = document.createElement("li");
              li.className = value ? "pass" : "fail";
              li.appendChild(document.createTextNode(desc));
              results.appendChild(li);
              if (!value) {
                li.parentNode.parentNode.className = "fail";
              }
              return li;
            };
          })();
          window.onload = function() {
            test("Async Test #1", function() {
              pause();
              setTimeout(function() {
                assert(true, "First test completed");
                resume();
              }, 1000);
            });
            test("Async Test #2", function() {
              pause();
              setTimeout(function() {
                assert(true, "Second test completed");
                resume();
              }, 1000);
            });
          };
        </script>
        <style>
          #results li.pass {
            color: green;
          }

          #results li.fail {
            color: red;
          }
        </style>
      </head>
      <body>
        <ul id="results"></ul>
      </body>
    </html>

    Let's break down the functionality exposed in listing 3. There are three publicly accessible functions: test(), pause(), and resume(). These three functions have the following capabilities:

    • test(fn) takes a function that contains a number of assertions—assertions that will be run either synchronously or asynchronously—and places it on the queue to await execution.
    • pause() should be called from within a test function and tells the test suite to pause executing tests until the test group is done.
    • resume() unpauses the tests and starts the next test running after a short delay designed to avoid long-running code blocks.

    The one internal implementation function, runTest(), is called whenever a test is queued or dequeued. It checks to see if the suite is currently unpaused and if there's something in the queue; in which case it'll dequeue a test and try to execute it. Additionally, after the test group is finished executing, runTest() will check to see if the suite is currently paused, and if it's not (meaning that only asynchronous tests were run in the test group) runTest() will begin executing the next group of tests.

    Summary

    In this article, we looked at how to construct a simple test suite capable of handling asynchronous test cases. Altogether, these techniques will serve as an important cornerstone to the rest of our development with JavaScript.


    Here are some other Manning titles you might be interested in:

    Ext JS in Action, Second Edition

    Ext JS in Action, Second Edition
    Jesus Garcia, Jacob K. Andresen, and Grgur Grisogono

    Third-Party JavaScript

    Third-Party JavaScript
    Ben Vinegar and Anton Kovalyov

    CoffeeScript in Action

    CoffeeScript in Action
    Patrick Lee


    AttachmentSize
    cueball1.png1.19 KB
    cueball2.png1.19 KB
    cueball3.png1.19 KB
    cueball4.png1.19 KB
    image009.jpg23.62 KB
    secrets001.png29.82 KB
    secrets002.jpg10.34 KB
    secrets003.jpg10.73 KB