Skip to content

cjohansen.no

Syntactical sugar for your JavaScript unit tests

After switching my Ruby tests from vanilla Test::Unit to Shoulda (via Jeremy McAnally's Context) a while back I've gotten quite used to sexy unit tests. Coming back to JsUnitTest for JavaScript last night, I realized the syntac wasn't working for me anymore. So today, I fixed it.

JsUnitTest

For those of you who don''t already know it, JsUnitTest is the unit test framework used by (and created by) the prototypejs guys. The version I'm linking to is modified by Dr. Nic to avoid depending on prototypejs (meaning you won't get conflicts if you test code written with other frameworks).

In January, I presented JsUnitTest and several other frameworks, check it out.

The problem

In JsUnitTest, test cases get their names from method names on an object literal:

new Test.Unit.Runner({
    testTrimTrailingSpace: function() { with(this) {
        assertEqual("String", trim("String "), "Trailing space should be removed");
    }},

    testTrimLeadingSpace: function() { with(this) {
        assertEqual("String", trim(" String"), "Leading space should be removed");
    }},

    testNoTrimInnerSpace: function() { with(this) {
        var sentence = "A complete sentence with     lots of whitespace.";
        assertEqual(sentence, trim(sentence), "Inner space should not be removed");
    }}
}, { testLog: "testlog" });

The runner looks like this (when failing) and this (when passing).

Introducing: JsContext

Notice how those method names appeared as the label? Who agrees with me that normal spaced sentences are easier to read than camelCasedStatements? Especially reallyLongOnesThatTryToAccuratelyDiscribeWhatsGoingOn. It's this itch that caused me to quickly whip up JsContext.

JsContext is a small JavaScript file, which hijacks JsUnitTest's JsUnitTest.Unit.Runner.prototype.getTests and adds in support for tests named "should..." as well as contexts.

The idea that hit me that caused all this was the fact that property names for JavaScript objects can be arbitrary strings. Realizing this, I quickly turned the above into

new Test.Unit.Runner({
    "test trim trailing space": function() { with(this) {
        assertEqual("String", trim("String "), "Trailing space should be removed");
    }},

    "test trim leading space": function() { with(this) {
        assertEqual("String", trim(" String"), "Leading space should be removed");
    }},

    "test no trim inner space": function() { with(this) {
        var sentence = "A complete sentence with     lots of whitespace.";
        assertEqual(sentence, trim(sentence), "Inner space should not be removed");
    }}
}, { testLog: "testlog" });

This already made more sense. However, it happened to start looking like Jeremy McAnally's Context (for Ruby), which led my thoughts to my enthusiasm for Shoulda, and so I immediately wanted more. JsUnitTest only runs test methods that start with "test", so in order to get "should" and contexts I needed to hack a little.

Using JsContext

Using JsContext is easy. You just fetch the file from GitHub, include it after jsunittest.js, and you're off:

<script src="jsunittest.js" type="text/javascript"></script>
<script src="jscontext.js" type="text/javascript"></script>

Now you can write the above example like this instead:

new Test.Unit.Runner({
    "should remove trailing space": function() { with(this) {
        assertEqual("String", trim("String "), "Trailing space should be removed");
    }},

    "should remove leading space": function() { with(this) {
        assertEqual("String", trim(" String"), "Leading space should be removed");
    }},

    "should not remove inner space": function() { with(this) {
        var sentence = "A complete sentence with     lots of whitespace.";
        assertEqual(sentence, trim(sentence), "Inner space should not be removed");
    }}
}, { testLog: "testlog" });

When you're tests start getting longer, you may want to group them. This is where contexts enter the game (example liftet from Context readme and adapted to JsUnitTest/JsContext):

new Test.Unit.Runner({
    "A new User": JsContext({
        "with clown shoes": JsContext({
            "should squeak": function() { with(this) {
                // ...
            }},

            // ...more tests and/or contexts
        }),

        "without clown shoes": JsContext({
            "should not squeak": function() { with(this) {
                // ...
            }},

            // ...more tests and/or contexts
        }),

        // ...more tests and/or contexts
    }),

    // ...more tests and/or contexts
}, { testLog: "testlog" });

In the test runner, contexts are appended to test names. This gives you nice output like "A new user with clown shoes should squeak" (in contrast to "testNewUserWithClownShoesShouldSqueak").

Let me repeat that this only adds syntactical sugar; your ability to test is exactly the same as it was. However, I think that for tests, syntactical sugar may be especially beneficial, since the sugars prime objective is to improve readability. This kind of expressiveness makes your tests easier to read and maintain, and thus, more likely to stay maintained.

Possibly related

2006 - 2012 Christian Johansen Creative Commons License