Anders Tornblad, web developer

I'm all about the web

Monthly archive for February 2013

JavaScript unit test framework, part 4

This is the last part of the first subject – my JavaScript unit test framework. The last thing I'm adding are just some extra bells and whistles:

  • Pluggable output system, using custom events

New requirements

  1. Events should be triggered on the window object at key points in time
    • When run() is called, the lbrtwUtEngineStarting event should be triggered
    • Before running one unit test, the lbrtwUtTestStarting event should be triggered
    • After running one unit test, the lbrtwUtTestEnded event should be triggered
    • After running the last unit test, the lbrtwUtEngineEnded event should be triggered
    • The lbrtwUtEngineStarting event object should contain the testCount property
    • The lbrtwUtTestStarting event object should contain the testName and testIndex properties
    • The lbrtwUtTestEnded event object should contain the testName, testIndex, testSuccess, testErrorMessage, testCount, successCount and failureCount properties
    • The lbrtwEngineEnded event object should contain the testCount, successCount, failureCount, testNames and failures properties

Tenth requirement

When testing for events being thrown to the window, it is important to clean up after oneself by removing all event listeners. These are the tests:

engine.add("Calling run() should trigger the lbrtwUtEngineStarting event once", function(testContext, the) { // Arrange var eng = new ut.Engine(); var triggered = 0; var onStarting = function(event) { window.removeEventListener("lbrtwUtEngineStarting", onStarting, false); triggered++; }; window.addEventListener("lbrtwUtEngineStarting", onStarting, false);   // Act testContext.actAndWait(100, function() { eng.run(); }).   // Assert thenAssert(function() { the(triggered).shouldBeExactly(1); }) });   engine.add("The lbrtwUtEngineStarting event object should contain testCount", function(testContext, the) { // Arrange var eng = new ut.Engine(); eng.add("a", function() {}); eng.add("b", function() {}); var testCount; var onStarting = function(event) { window.removeEventListener("lbrtwUtEngineStarting", onStarting, false); testCount = event.testCount; }; window.addEventListener("lbrtwUtEngineStarting", onStarting, false);   // Act testContext.actAndWait(100, function() { eng.run(); }).   // Assert thenAssert(function() { the(testCount).shouldBeExactly(2); }) });   engine.add("lbrtwUtTestStarting event should be triggered once per unit test", function(testContext, the) { // Arrange var eng = new ut.Engine(); var triggered = {}; var triggeredByIndex = []; eng.add("a", function() {}); eng.add("b", function() {}); var onTestStarting = function(event) { triggered[event.testName] = (triggered[event.testName] || 0) + 1; triggeredByIndex[event.testIndex] = (triggeredByIndex[event.testIndex] || 0) + 1; }; window.addEventListener("lbrtwUtTestStarting", onTestStarting, false);   // Act testContext.actAndWait(100, function() { eng.run().then(function() { window.removeEventListener("lbrtwUtTestStarting", onTestStarting, false); }); }).   // Assert thenAssert(function() { the("a").propertyOf(triggered).shouldBeExactly(1); the("b").propertyOf(triggered).shouldBeExactly(1); the(0).propertyOf(triggeredByIndex).shouldBeExactly(1); the(1).propertyOf(triggeredByIndex).shouldBeExactly(1); }) });   engine.add("lbrtwUtTestEnded event should be triggered once per unit test", function(testContext, the) { // Arrange var eng = new ut.Engine(); var results = {}; eng.add("a", function() {}); eng.add("b", function() { throw "Crash!"; }); var onTestEnded = function(event) { results[event.testName] = { success : event.testSuccess, errorMessage : event.testErrorMessage, successCount : event.successCount, failureCount : event.failureCount, testCount : event.testCount }; }; window.addEventListener("lbrtwUtTestEnded", onTestEnded, false);   // Act testContext.actAndWait(100, function() { eng.run().then(function() { window.removeEventListener("lbrtwUtTestEnded", onTestEnded, false); }); }).   // Assert thenAssert(function() { the("a").propertyOf(results).shouldBeTruthy(); the("success").propertyOf(results.a).shouldBeTrue(); the("errorMessage").propertyOf(results.a).shouldBeNull(); the("successCount").propertyOf(results.a).shouldBeExactly(1); the("failureCount").propertyOf(results.a).shouldBeExactly(0); the("testCount").propertyOf(results.a).shouldBeExactly(2);   the("b").propertyOf(results).shouldBeTruthy(); the("success").propertyOf(results.b).shouldBeFalse(); the("errorMessage").propertyOf(results.b).shouldBeExactly("Crash!"); the("successCount").propertyOf(results.b).shouldBeExactly(1); the("failureCount").propertyOf(results.b).shouldBeExactly(1); the("testCount").propertyOf(results.b).shouldBeExactly(2); }) });   engine.add("The lbrtwUtEngineEnded event object should contain testCount, failureCount, successCount, testNames and failures", function(testContext, the) { // Arrange var eng = new ut.Engine(); eng.add("a", function() {}); eng.add("b", function() {}); eng.add("c", function() { throw "Crash!"; }); var testCount; var failureCount; var successCount; var testNames; var failures;   var onEnded = function(event) { window.removeEventListener("lbrtwUtEngineEnded", onEnded, false); testCount = event.testCount; failureCount = event.failureCount; successCount = event.successCount; testNames = event.testNames; failures = event.failures; }; window.addEventListener("lbrtwUtEngineEnded", onEnded, false);   // Act testContext.actAndWait(100, function() { eng.run(); }).   // Assert thenAssert(function() { the(testCount).shouldBeExactly(3); the(failureCount).shouldBeExactly(1); the(successCount).shouldBeExactly(2); the("length").propertyOf(testNames).shouldBeExactly(3); the("length").propertyOf(failures).shouldBeExactly(1); }) });

The implementation is actually pretty easy. I write a helper function for dispatching my custom events, and then I use it at various points during the test process. Here is the helper function. Download the code below to see the complete implementation.

var triggerEvent = function(name, properties) { var event = document.createEvent("HTMLEvents"); event.initEvent(name, true, false);   for (var key in properties) { event[key] = properties[key]; }   window.dispatchEvent(event); };   // call like this: // triggerEvent("lbrtwUtTestStarting", { // testName : name, // testIndex : index // });

Making the output pretty

By listening to the four events dispatched by the framework it is possible to create any output you like. When you download the code below, you will find an example of what you can do.

Sending results to your server

By listening to the lbrtwUtEngineEnded event, I can now send the complete test results to any server using XMLHttpRequest directly or using any of the jQuery.ajax shortcuts.

$.on("lbrtwUtEngineEnded", function(event) { var oev = event.originalEvent;   $.post("http://myserver/unit-test-results", { userAgent : navigator.userAgent, testCount : oev.testCount, successCount : oev.successCount, failureCount : oev.failureCount });   // Of course, you can also use the failures and testNames properties });   myEngine.run();

The finished code can be found on github at /lbrtw/ut

JavaScript unit test framework, part 1
JavaScript unit test framework, part 2
JavaScript unit test framework, part 3
JavaScript unit test framework, part 4 (this part)

JavaScript unit test framework, part 3

Any mature unit test framework contains lots of helper methods for validating requirements. Instead of manually validating values and references using if and then throwing errors when requirements aren't satisfied, one should use helper methods.

I would like to be able to do this:

eng.add("Test stuff", function(testContext, the) { // Arrange var obj = new SomeClass();   // Act var result = obj.doStuff(1, 2, 3);   // Assert the(result).shouldNotBeNull(); the(result).shouldBeInstanceOf(ResultClass); the("foo").propertyOf(result).shouldBeLessThan(1000); the("bar").propertyOf(result).shouldBeExactly(1); the("abc").propertyOf(result).shouldBeTrue(); // and so on... });   eng.add("Test that a method throws an error", function(testContext, the) { // Arrange var obj = new SomeClass();   // Act and assert the("doStuff").methodOf(obj).withArguments(1, 2, 3).shouldThrowAnError(); });

When some should* method discovers a failing test, it should throw an exception to signal the failure to the test engine. I'm thinking that the shouldThrowAnError track would be the best to develop first, because it would make all future tests easier to write.

If the object or function being tested is named through the methodOf() or propertyOf() methods, and the requirement is not fulfilled, the error message should contain the name of the object or function being tested.

New requirements

  1. The second argument to a test function should be the ut.The function
  2. The ut.The function should simplify unit testing
    • calling the(func).shouldThrowAnError() should call func()
    • calling the(func).withArguments(...).shouldThrowAnError() should call func(...)
    • calling the(func).shouldThrowAnError() should throw an error if and only if func() does not throw an error
    • calling the(methodname).methodOf(object).shouldThrowAnError() should call object[methodname]()
    • calling the(x).shouldBeNull() should throw an error if x is not null
    • calling the(x).shouldNotBeNull() should throw an error if x is null
    • calling the(x).shouldBeExactly(y) should throw an error if x is not exactly y
    • calling the(x).shouldNotBeExactly(y) should throw an error if x is exactly y
    • calling the(x).shouldBeTrue() should throw an error if x is not true
    • calling the(x).shouldBeFalse() should throw an error if x is not false
    • calling the(x).shouldBeTruthy() should throw an error if x is not truthy
    • calling the(x).shouldBeFalsy() should throw an error if x is not falsy
    • calling the(x).shouldBeGreaterThan(y) should throw an error if x is not greater than y
    • calling the(x).shouldBeLessThan(y) should throw an error if x is not less than y
    • calling the(x).shouldBeGreaterThanOrEqualTo(y) should throw an error if x is not greater than or equal to y
    • calling the(x).shouldBeLessThanOrEqualTo(y) should throw an error if x is not less than or equal to y
    • calling the(x).shouldBeInstanceOf(y) should throw an error if x is not an instance of the y class
    • calling the("x").methodOf(y) should make an error message contain the text The x method
    • calling the("x").propertyOf(y) should make an error message contain the text The x property

Eighth requirement

engine.add("second argument to test functions should be the ut.The function", function() { // Arrange var eng = new ut.Engine(); var theThe; eng.add("Get the The", function(first, second) { theThe = second; });   // Act eng.run();   // Assert if (!theThe) { throw "Second argument null or missing"; } if (theThe !== ut.The) { throw "Second argument isn't ut.The"; } }); var utThe = function() { };   var runOneTestOrAssertFunc = function(engine, func, context) { try { func.call(null, context, utThe); if (!engine.actAndWaitFunc) { ++engine.successCount; } } catch(e) { engine.failures.push({ name : engine.currentTestName, message : e.toString() }); ++engine.failureCount; } };   window.ut = { "Engine" : utEngine, "TestContext" : utTestContext, "The" : utThe };

Ninth requirement

Requirement 9 – first batch

The first batch focuses on the shouldThrowAnError function. It should simply call the function passed to the the function, then examine the results. If there is an error, everything is fine, otherwise an error should be thrown because the expected error did not occur. This batch also deals with extracting a named method from an object using the methodOf function, and passing arguments to a function using the withArguments function.

engine.add("calling the(func).shouldThrowAnError() should call func()", function() { // Arrange var called = false; var func = function() { called = true; };   // Act try { ut.The(func).shouldThrowAnError(); } catch(e) { }   // Assert if (!called) { throw "func() wasn't called!"; } });   engine.add("calling the(func).withArguments(...).shouldThrowAnError() should call func(...)", function() { // Arrange var theA, theB, theC; var func = function(a, b, c) { theA = a; theB = b; theC = c; };   // Act try { ut.The(func).withArguments(1, 2, 3).shouldThrowAnError(); } catch(e) { }   // Assert if (theA !== 1) { throw "First argument was not passed"; } if (theB !== 2) { throw "Second argument was not passed"; } if (theC !== 3) { throw "Third argument was not passed"; } });   engine.add("the('foo').methodOf(bar) should be the same as the(bar.foo)", function() { // Arrange var called = false; var bar = { foo : function() { called = true; } };   // Act try { ut.The("foo").methodOf(bar).shouldThrowAnError(); } catch(e) { }   // Assert if (!called) { throw "bar.foo() was not called"; } });   engine.add("the('foo').methodOf(bar) should be the same as the(bar.foo), part 2", function() { // Arrange var theA, theB; var bar = { foo : function(a, b) { theA = a; theB = b; } };   // Act try { ut.The("foo").methodOf(bar).withArguments(1, 2).shouldThrowAnError(); } catch(e) { }   // Assert if (theA !== 1 || theB !== 2) { throw "bar.foo(1, 2) was not called"; } });   engine.add("shouldThrowAnError() should not throw an error if some error was thrown", function() { // Arrange var errorThrown = false; var func = function() { throw "Expected failure"; };   // Act try { ut.The(func).shouldThrowAnError(); } catch (e) { errorThrown = true; }   // Assert if (errorThrown) { throw "An error was thrown!"; } });   engine.add("shouldThrowAnError() should throw an error if no error was thrown", function() { // Arrange var errorThrown = false; var func = function() { };   // Act try { ut.The(func).shouldThrowAnError(); } catch (e) { errorThrown = true; }   // Assert if (!errorThrown) { throw "No error was thrown!"; } }); var asserter = function(arg) { this.target = arg; };   asserter.prototype = { methodOf : function(obj) { this.target = (function(obj, name) { return obj[name]; })(obj, this.target);   return this; },   withArguments : function() { var args = [].slice.call(arguments);   this.target = (function(method, args) { return function() { method.apply(null, args); }; })(this.target, args);   return this; },   shouldThrowAnError : function() { var threw = false; try { this.target.call(); } catch (e) { threw = true; } if (!threw) { throw "Did not throw an error"; } } };   var utThe = function(arg) { return new asserter(arg); };

Requirement 9 – second batch

The last few tests for this time validate all the comparison functions, like shouldBeNull() and shouldBeGreaterThan(). These should throw descriptive error messages, so I test the actual message texts.

Instead of writing every single unit test separately for the validation methods (which would have me writing 30+ unit tests by hand), I write a helper function to create three unit tests per validation method: One for validating that no error is thrown when no error should be thrown, and two for validating the correct error messages.

engine.add("the('foo').methodOf(bar) should be the same as the(bar.foo), but change the error message", function() { // Arrange var errorThrown = false; var bar = { foo : function() { } };   // Act try { ut.The("foo").methodOf(bar).shouldThrowAnError(); } catch (e) { errorThrown = e.toString(); }   // Assert if (!errorThrown) { throw "No error was thrown!"; } var expected = "The foo method did not throw an error"; if (errorThrown != expected) { throw "The wrong error was thrown! Expected: '" + expected + "', actual: '" + errorThrown + "'"; } });   var addAssertTestsForMethod = function(engine, methodName, goodValue, badValue, arg, expectedError) { if (typeof goodValue != "undefined") { var passingTestName = methodName + "() should not throw an error";   var passingTestFunc = (function(methodName, goodValue, arg) { return function() { // Act ut.The(goodValue)[methodName](arg); }; })(methodName, goodValue, arg);   engine.add(passingTestName, passingTestFunc); } if (typeof badValue != "undefined") { var failingTestName = methodName + "() should throw an error"; var namedFailingTestName = methodName + "() should throw an error with correct name";   var failingTestFunc = (function(methodName, badValue, arg, expectedError) { return function() { // Arrange var errorThrown = false;   // Act try { ut.The(badValue)[methodName](arg); } catch (e) { errorThrown = e; }   // Assert if (!errorThrown) { throw "Did not throw an error"; } if (errorThrown != expectedError) { throw "Did not throw the correct error. Expected: '" + expectedError + "', actual: '" + errorThrown + "'"; } }; })(methodName, badValue, arg, expectedError.replace(/%/, "The value"));   var namedFailingTestFunc = (function(methodName, badValue, arg, expectedError) { return function() { // Arrange var errorThrown = false; var obj = { prop : badValue };   // Act try { ut.The("prop").propertyOf(obj)[methodName](arg); } catch (e) { errorThrown = e; }   // Assert if (!errorThrown) { throw "Did not throw an error"; } if (errorThrown != expectedError) { throw "Did not throw the correct error. Expected: '" + expectedError + "', actual: '" + errorThrown + "'"; } }; })(methodName, badValue, arg, expectedError.replace(/%/, "The prop property"));   engine.add(failingTestName, failingTestFunc); engine.add(namedFailingTestName, namedFailingTestFunc); } };   var testClass = function() { this.foo = "bar"; };   addAssertTestsForMethod(engine, "shouldBeNull", null, 123, undefined, "% is not null"); addAssertTestsForMethod(engine, "shouldNotBeNull", 123, null, undefined, "% is null"); addAssertTestsForMethod(engine, "shouldBeExactly", 1, true, 1, "Expected: exactly 1, %: true"); addAssertTestsForMethod(engine, "shouldNotBeExactly", true, 1, 1, "% is exactly 1"); addAssertTestsForMethod(engine, "shouldBeLessThan", 1, 2, 2, "Expected: less than 2, %: 2"); addAssertTestsForMethod(engine, "shouldBeGreaterThan", 3, 2, 2, "Expected: greater than 2, %: 2"); addAssertTestsForMethod(engine, "shouldBeLessThanOrEqualTo", 2, 3, 2, "Expected: less than or equal to 2, %: 3"); addAssertTestsForMethod(engine, "shouldBeGreaterThanOrEqualTo", 2, 1, 2, "Expected: greater than or equal to 2, %: 1"); addAssertTestsForMethod(engine, "shouldBeTrue", true, 1, undefined, "% is not true"); addAssertTestsForMethod(engine, "shouldBeTruthy", 1, false, undefined, "% is not truthy"); addAssertTestsForMethod(engine, "shouldBeFalse", false, 0, undefined, "% is not false"); addAssertTestsForMethod(engine, "shouldBeFalsy", 0, true, undefined, "% is not falsy"); addAssertTestsForMethod(engine, "shouldBeInstanceOf", new testClass(), testClass, testClass, "% is not of correct type"); var asserter = function(arg) { this.target = arg; this.valueName = "The value"; this.methodName = "The function"; };   asserter.prototype = { methodOf : function(obj) { this.methodName = "The " + this.target + " method";   this.target = (function(obj, name) { return obj[name]; })(obj, this.target);   return this; },   withArguments : function() { var args = [].slice.call(arguments);   this.target = (function(method, args) { return function() { method.apply(null, args); }; })(this.target, args);   return this; },   shouldThrowAnError : function() { var threw = false; try { this.target.call(); } catch (e) { threw = true; } if (!threw) { throw this.methodName + " did not throw an error"; } },   propertyOf : function(obj) { this.valueName = "The " + this.target + " property"; this.target = obj[this.target];   return this; },   shouldBeNull : function() { if (this.target !== null) { throw this.valueName + " is not null"; } },   shouldNotBeNull : function() { if (this.target === null) { throw this.valueName + " is null"; } },   shouldBeExactly : function(arg) { if (this.target !== arg) { throw "Expected: exactly " + arg + ", " + this.valueName + ": " + this.target; } },   shouldNotBeExactly : function(arg) { if (this.target === arg) { throw this.valueName + " is exactly " + arg; } },   shouldBeLessThan : function(arg) { if (!(this.target arg)) { throw "Expected: greater than " + arg + ", " + this.valueName + ": " + this.target; } },   shouldBeLessThanOrEqualTo : function(arg) { if (!(this.target = arg)) { throw "Expected: greater than or equal to " + arg + ", " + this.valueName + ": " + this.target; } },   shouldBeTrue : function() { if (this.target !== true) { throw this.valueName + " is not true"; } },   shouldBeTruthy : function() { if (!this.target) { throw this.valueName + " is not truthy"; } },   shouldBeFalse : function() { if (this.target !== false) { throw this.valueName + " is not false"; } },   shouldBeFalsy : function() { if (this.target) { throw this.valueName + " is not falsy"; } },   shouldBeInstanceOf : function(theClass) { if (!(this.target instanceof theClass)) { throw this.valueName + " is not of correct type"; } } };

There we go. This is actually all I'm going to do for the actual testing of things. The next and final post will deal with better output of test results, including sending test result data to your server of choice. I will also add some pluggability to the unit test framework.

The finished code can be found on github at /lbrtw/ut

JavaScript unit test framework, part 1
JavaScript unit test framework, part 2
JavaScript unit test framework, part 3 (this part)
JavaScript unit test framework, part 4