Unit Testing with QUnit

bocoup

bocoup

“Cowboy” Ben Alman

benalman.com
github.com/cowboy
@cowboy

What is QUnit?

An easy-to-use JavaScript
Unit Testing framework

How easy?

Very easy.

    test("some tests", function() {
      expect(3);

      ok(true, "passes because true is true");
      equal("1", 1, "passes because '1' == 1");
      strictEqual("1", 1, "fails because '1' !== 1");
    });
  

Used and Maintained
by the jQuery Project

Thousands of tests.

Why Unit Test?

Does Your Code Work?

Fixing Bugs

  1. Write a test that asserts a bug's existence.
  2. Squash the bug.
  3. Pat self on back.

Regression Testing

  1. Change some code.
  2. Ensure you didn't break anything.
  3. Pat self on back.

It's Cool

  1. Devs know that you're responsible.
  2. They use your code with confidence.
  3. Pat self on back.

Getting Started

Download QUnit

Three Files

Just qunit.js, qunit.css, and a little bit of HTML:

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>MyApp Test Suite</title>
      <link rel="stylesheet" href="qunit.css" type="text/css">
      <script src="qunit.js"></script>
      <script src="myapp.js"></script>
      <script src="myapp-test.js"></script>
    </head>
    <body>
      <h1 id="qunit-header">MyApp Test Suite</h1>
      <h2 id="qunit-banner"></h2>
      <div id="qunit-testrunner-toolbar"></div>
      <h2 id="qunit-userAgent"></h2>
      <ol id="qunit-tests"></ol>
      <div id="qunit-fixture"></div>
    </body>
    </html>
  

Create a Test

It's really this simple.

    test("The name of the test", function() {
      // Assertions.
    });
  

What are Assertions?

You say “this is how it should work,”
and QUnit tells you when it doesn't.

First, Set Expectations

Get in the habit of doing this!

    // You can either set an expectation (number) like this.

    test("test name", function() {
      expect(3);

      // QUnit expects 3 assertions in this test.
    });

    // Or like this.

    test("test name", 3, function() {
      // QUnit expects 3 assertions in this test.
    });

  

QUnit Assertions

ok, equal, notEqual,
strictEqual, notStrictEqual,
deepEqual, notDeepEqual, raises

ok

A boolean assertion that passes if the first argument is truthy.

    test("ok", 3, function() {
      ok(true,  "passes because true is true");
      ok(1,     "passes because 1 is truthy");
      ok("",    "fails because empty string is not truthy");
    });
  

equal

A comparison assertion that passes if actual == expected.

    test("equal", 3, function() {
      var actual = 5 - 4;

      equal(actual, 1,     "passes because 1 == 1");
      equal(actual, true,  "passes because 1 == true");
      equal(actual, false, "fails because 1 != false");
    });
  

notEqual

A comparison assertion that passes if actual != expected.

    test("notEqual", 3, function() {
      var actual = 5 - 4;

      notEqual(actual, 0,     "passes because 1 != 0");
      notEqual(actual, false, "passes because 1 != false");
      notEqual(actual, true,  "fails because 1 == true");
    });
  

strictEqual

A comparison assertion that passes if actual === expected.

    test("strictEqual", 3, function() {
      var actual = 5 - 4;

      strictEqual(actual, 1,     "passes because 1 === 1");
      strictEqual(actual, true,  "fails because 1 !== true");
      strictEqual(actual, false, "fails because 1 !== false");
    });
  

notStrictEqual

A comparison assertion that passes if actual !== expected.

    test("notStrictEqual", 3, function() {
      var actual = 5 - 4;

      notStrictEqual(actual, 1,     "fails because 1 === 1");
      notStrictEqual(actual, true,  "passes because 1 !== true");
      notStrictEqual(actual, false, "passes because 1 !== false");
    });
  

deepEqual

Recursive comparison assertion, working on primitives, arrays and objects, using ===.

    test("deepEqual", 7, function() {
      var actual = {a: 1};

      equal(    actual, {a: 1},   "fails because objects are different");
      deepEqual(actual, {a: 1},   "passes because objects are equivalent");
      deepEqual(actual, {a: "1"}, "fails because '1' !== 1");

      var a = $("body > *");
      var b = $("body").children();

      equal(    a,       b,       "fails because jQuery objects are different");
      deepEqual(a,       b,       "fails because jQuery objects are not equivalent");
      equal(    a.get(), b.get(), "fails because element arrays are different");
      deepEqual(a.get(), b.get(), "passes because element arrays are equivalent");
    });
  

notDeepEqual

Recursive comparison assertion. The result of deepEqual, inverted.

    test("notDeepEqual", 3, function() {
      var actual = {a: 1};

      notEqual(    actual, {a: 1},   "passes because objects are different");
      notDeepEqual(actual, {a: 1},   "fails because objects are equivalent");
      notDeepEqual(actual, {a: "1"}, "passes because '1' !== 1");
    });
  

raises

Assertion to test if a callback throws an exception when run.

    test("raises", 3, function() {
      raises(function() {
        throw new Error("Look ma, I'm an error!");
      }, "passes because an error is thrown inside the callback");

      raises(function() {
        x // ReferenceError: x is not defined
      }, "passes because an error is thrown inside the callback");

      raises(function() {
        var a = 1;
      }, "fails because no error is thrown inside the callback");
    });
  

Tests Should be Atomic

Execution order cannot be guaranteed!

    // Don't do this.

    var counter = 0;

    test("first test", 1, function() {
      counter++;
      equal(counter, 1, "counter should be 1");
    });

    test("second test", 1, function() {
      counter++;
      equal(counter, 2, "counter should be 2");
    });

    test("third test", 2, function() {
      counter++;
      equal(counter, 2, "counter should be 2");
      ok(false, "oops, an error");
    });
  

DOM Testing

#qunit-fixture

Any markup in here will be reset after every test (uses jQuery if available).

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8" />
      <title>MyApp Test Suite</title>
      <link rel="stylesheet" href="qunit.css" type="text/css">
      <script src="qunit.js"></script>
      <script src="myapp.js"></script>
      <script src="myapp-test.js"></script>
    </head>
    <body>
      <h1 id="qunit-header">MyApp Test Suite</h1>
      <h2 id="qunit-banner"></h2>
      <div id="qunit-testrunner-toolbar"></div>
      <h2 id="qunit-userAgent"></h2>
      <ol id="qunit-tests"></ol>
      <div id="qunit-fixture">
        <ul>
          <li>foo</li>
          <li>bar</li>
          <li>baz</li>
        </ul>
      </div>
    </body>
    </html>
  

A Simple “Test Suite”

A little forethought can save a lot of frustration.

    module("jQuery#enumerate");

    test("chainable", 1, function() {
      var items = $("#qunit-fixture li");
      strictEqual(items.enumerate(), items, "should be chaninable");
    });

    test("no args passed", 3, function() {
      var items = $("#qunit-fixture li").enumerate();
      equal(items.eq(0).text(), "1. foo", "first item should have index 1");
      equal(items.eq(1).text(), "2. bar", "second item should have index 2");
      equal(items.eq(2).text(), "3. baz", "third item should have index 3");
    });

    test("0 passed", 3, function() {
      var items = $("#qunit-fixture li").enumerate(0);
      equal(items.eq(0).text(), "0. foo", "first item should have index 0");
      equal(items.eq(1).text(), "1. bar", "second item should have index 1");
      equal(items.eq(2).text(), "2. baz", "third item should have index 2");
    });

    test("1 passed", 3, function() {
      var items = $("#qunit-fixture li").enumerate(1);
      equal(items.eq(0).text(), "1. foo", "first item should have index 1");
      equal(items.eq(1).text(), "2. bar", "second item should have index 2");
      equal(items.eq(2).text(), "3. baz", "third item should have index 3");
    });
  

Now, when I refactor...

(Time for an example)

Plugin Authors

If it makes sense, test your plugin
in older versions of jQuery too.

Organization

Create Modules

Because your unit tests should be organized too.

    module("core");

    test("a test in the core module", function() {
      ok(true, "this test had better pass");
    });

    test("another test in the core module", function() {
      ok(true, "this test had also better pass");
    });

    module("options");

    test("a test in the options module", function() {
      ok(true, "this test really, really better pass");
    });

    test("another test in the options module", function() {
      ok(false, "sadly, this test is going to fail");
    });
  

More Powerful Modules

Configure setup and teardown callbacks to streamline your tests.

    // Defining a "setup" callback.

    module("module1", {
      setup: function() {
        ok(true, "once extra assert per test");
      }
    });

    test("test with setup", function() {
      expect(1);
    });


    // Defining both "setup" and "teardown" callbacks.

    module("module2", {
      setup: function() {
        ok(true, "once extra assert per test");
        this.prop = "foo";
      },
      teardown: function() {
        ok(true, "and one extra assert after each test");
      }
    });

    test("test with setup and teardown", function() {
      expect(3);
      same(this.prop, "foo", "this.prop === 'foo' in all tests");
    });
  

Asynchronous Testing

Sweet, no errors!

But as you can see, sometimes no errors is a bad thing.

    test("no errors", function() {
      var actual = false;

      setTimeout(function() {
        ok(actual, "this test would fail.. if it ever ran");
      }, 1000);
    });
  

Set Expectations!

Now you get an error.. but it's not the error you want.

    test("expectations", function() {
      expect(1);

      var actual = false;

      setTimeout(function() {
        ok(actual, "this test would fail.. if it ever ran");
      }, 1000);
    });
  

stop & start

You must tell QUnit to wait for an asynchronous action to complete.

    test("stop & start", function() {
      expect(1);

      var actual = false;

      stop();
      setTimeout(function() {
        ok(actual, "this test actually runs, and fails");
        start();
      }, 1000);
    });
  

asyncTest

Another way to tell QUnit to wait for an asynchronous action to complete.

    asyncTest("asyncTest & start", function() {
      expect(1);

      var actual = false;

      setTimeout(function() {
        ok(actual, "this test actually runs, and fails");
        start();
      }, 1000);
    });
  

stops & starts

Jörn added this in... because I asked nicely.

    test("stops & starts", function() {
      expect(4);

      var url = "http://jsfiddle.net/echo/jsonp/?callback=?";

      stop();
      $.getJSON(url, {a: 1}, function(data) {
        ok(data, "data is returned from the server");
        equal(data.a, "1", "the value of data.a should be 1");
        start();
      });

      stop();
      $.getJSON(url, {b: 2}, function(data) {
        ok(data, "data is returned from the server");
        equal(data.b, "2", "the value of data.b should be 2");
        start();
      });
    });
  

Simulate AJAX

What are you testing anyways, your
client code or your server code?

Mocking AJAX

If you mock your AJAX Requests, you test your JavaScript, not your server.

    // Simulate your API.

    $.mockAjax("json", {
      "/user": {status: -1},
      "/user/(\\d+)": function(matches) {
        return {status: 1, user: "sample user " + matches[1]};
      }
    });

    // Unit tests.

    test("user tests", function() {
      expect(5);

      stop();
      $.getJSON("/user", function(data) {
        ok(data, "data is returned from the server");
        equal(data.status, "-1", "no user specified, status should be -1");
        start();
      });

      stop();
      $.getJSON("/user/123", function(data) {
        ok(data, "data is returned from the server");
        equal(data.status, "1", "user found, status should be 1");
        equal(data.user, "sample user 123", "user found, id should be 123");
        start();
      });
    });

  

URL Parameters

Running Specific Tests

Add ?filter=foo to the URL to run only tests whose names contain "foo".

“Modules” Test Suite
All tests modules.html
“core” module modules.html?filter=core
“options” module modules.html?filter=options
“Assertions” Test Suite
All tests assertions.html
“ok” test assertions.html?filter=ok
“equal” tests assertions.html?filter=equal

Global Pollution

Just add ?noglobals to the URL, and QUnit will fail any “leaky” tests.

    test("not leaky", 1, function() {
      var x = true;
      ok(x, "passes because x is true");
    });

    test("leaky", 1, function() {
      x = true;
      ok(x, "also passes because x is true");
    });
  

Automation

Callbacks

Automatically capture QUnit test results, à la TestSwarm.

    // Runs once at the very beginning.

    QUnit.begin = function() {
      console.log("Running Test Suite");
    };

    // Runs once at the very end.

    QUnit.done = function(failures, total) {
      console.info("Suite: %d failures / %d tests", failures, total);
    };

    // Runs once after each assertion.

    QUnit.log = function(result, message) {
      console[ result ? "log" : "error" ](message);
    };

    // Runs before each test.

    QUnit.testStart = function(name) {
      console.group("Test: " + name);
    };

    // Runs after each test.

    QUnit.testDone = function(name, failures, total) {
      console.info("Test: %d failures / %d tests", failures, total);
      console.groupEnd();
    };

    // Runs before each module.

    QUnit.moduleStart = function(name) {
      console.group("Module: " + name);
    };

    // Runs after each module.

    QUnit.moduleDone = function(name, failures, total) {
      console.info("Module: %d failures / %d tests", failures, total);
      console.groupEnd();
    };

    // Runs after each test group. Redefining this function will
    // override the built-in #qunit-fixture reset logic.

    QUnit.reset = function() {
      console.log("Test done!");
    };
  

TestSwarm

Distributed Continuous Integration for JavaScript.

TestSwarm

QUnit Recap