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.
Comments
Christian
30. March, 19:18
http://sugartest.scriptia.net/
http://jshoulda.scriptia.net/
As I said there, the important part is that you test. The more comfortable you are with your framework, the more you'll enjoy testing, which is a good thing :)
Aslak Hellesøy
(http://blog.aslakhellesoy.com/)
31. March, 01:24
http://github.com/jeresig/env-js/tree/master (or some of the more recent forks)
Christian
31. March, 08:20
Definitely will check this out tomorrow and get back to you.
Comments are closed