JavaScript unit test framework, part 4 of 4
This is part 4 of the JavaScript unit test framework series. If you haven't read the first part, here it is: JavaScript unit test framework, part 1 of 4
This is the last part of 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
- Events should be triggered on the
window
object at key points in time- When
run()
is called, theamtUtEngineStarting
event should be triggered - Before running one unit test, the
amtUtTestStarting
event should be triggered - After running one unit test, the
amtUtTestEnded
event should be triggered - After running the last unit test, the
amtUtEngineEnded
event should be triggered - The
amtUtEngineStarting
event object should contain thetestCount
property - The
amtUtTestStarting
event object should contain thetestName
andtestIndex
properties - The
amtUtTestEnded
event object should contain thetestName
,testIndex
,testSuccess
,testErrorMessage
,testCount
,successCount
andfailureCount
properties - The
amtEngineEnded
event object should contain thetestCount
,successCount
,failureCount
,testNames
andfailures
properties
- When
Tenth requirement
When testing for events being triggered on the window
, it is important to clean up after oneself by removing all event listeners. These are the tests:
// Tests
engine.add("Calling run() should trigger the amtUtEngineStarting event once",
function(testContext, the) {
// Arrange
var eng = new ut.Engine();
var triggered = 0;
var onStarting = function(event) {
window.removeEventListener("amtUtEngineStarting", onStarting, false);
triggered++;
};
window.addEventListener("amtUtEngineStarting", onStarting, false);
// Act
testContext.actAndWait(100, function() { eng.run(); }).
// Assert
thenAssert(function() {
the(triggered).shouldBeExactly(1);
})
});
engine.add("The amtUtEngineStarting 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("amtUtEngineStarting", onStarting, false);
testCount = event.testCount;
};
window.addEventListener("amtUtEngineStarting", onStarting, false);
// Act
testContext.actAndWait(100, function() { eng.run(); }).
// Assert
thenAssert(function() {
the(testCount).shouldBeExactly(2);
})
});
engine.add("amtUtTestStarting 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("amtUtTestStarting", onTestStarting, false);
// Act
testContext.actAndWait(100, function() { eng.run().then(function() {
window.removeEventListener("amtUtTestStarting", 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("amtUtTestEnded 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("amtUtTestEnded", onTestEnded, false);
// Act
testContext.actAndWait(100, function() { eng.run().then(function() {
window.removeEventListener("amtUtTestEnded", 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 amtUtEngineEnded 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("amtUtEngineEnded", onEnded, false);
testCount = event.testCount;
failureCount = event.failureCount;
successCount = event.successCount;
testNames = event.testNames;
failures = event.failures;
};
window.addEventListener("amtUtEngineEnded", 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.
// 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("amtUtTestStarting", {
// 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 amtUtEngineEnded
event, I can now send the complete test results to any server using XMLHttpRequest
directly or using any of the jQuery.ajax
shortcuts.
// Example
$.on("amtUtEngineEnded", 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 latest version of the code is always available in the GitHub repository.
Articles in this series: