source/class/core/testrunner/Test.js
/*
==================================================================================================
Core - JavaScript Foundation
Copyright 2012-2014 Sebastian Werner
==================================================================================================
*/
"use strict";
/**
* Wraps a function to test so that it can report back
* to a whole suite of tests and is protected regarding
* exceptions so that these do not influence the outside.
*/
core.Class("core.testrunner.Test",
{
/**
* - @title {String} Human readable title of the test
* - @func {Function} Function to test
* - @suite {core.testrunner.Suite} Instance of suite to assign to (for back reporting)
* - @total {Integer?null} Configure the total number of assertions which are expected to run
* - @timeout {Integer?null} Configure timeout to enable async execution of test
*/
construct : function(title, func, suite, total, timeout)
{
this.__title = title;
this.__func = func;
this.__suite = suite;
this.__totalCount = total;
this.__timeout = timeout;
/** List of passed/failed assertions */
this.__items = [];
},
members :
{
/*
----------------------------------------------
HELPER
----------------------------------------------
*/
/** {=String} Reason of failure, value is `null` when successful */
__failureReason : null,
/** {=String} Message to describe the failure */
__failureMessage : null,
/** Number of passed assertions */
__passedCount : 0,
/** Number of failed assertions */
__failedCount : 0,
/**
* Helper method to track new passed assertions with @message {String?""}.
*/
__passed : function(message)
{
this.__items.push({
passed : true,
message : message,
stacktrace: null
});
this.__passedCount++;
},
/**
* Helper method to track new failed assertions with @message {String?""} and
* an optional exception @ex {Exception?null}.
*/
__failed : function(message, ex)
{
var combined = message || "";
if (ex && combined != ex.message)
{
if (combined) {
combined += ": ";
}
combined += ex.message;
}
this.__items.push({
passed : false,
message : combined,
stacktrace : ex && ex.stack || null
});
this.__failedCount++;
},
/*
----------------------------------------------
ASSERTION API FOR TEST
----------------------------------------------
*/
/**
* Test whether @a {var} and @b {var} are equal and register
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isEqual : function(a, b, message)
{
try{
core.Assert.isEqual(a, b);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @a {var} and @b {var} are not equal and register
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isNotEqual : function(a, b, message)
{
try{
core.Assert.isNotEqual(a, b);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @a {var} and @b {var} are identical and register
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isIdentical : function(a, b, message)
{
try{
core.Assert.isIdentical(a, b);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @a {var} and @b {var} are not identical and register
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isNotIdentical : function(a, b, message)
{
try{
core.Assert.isNotIdentical(a, b);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @a {var} is truish and registers
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isTrue : function(a, message)
{
try{
core.Assert.isTrue(a);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @a {var} is falsy and registers
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isFalse : function(a, message)
{
try{
core.Assert.isFalse(a);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @a {var} is null and registers
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isNull : function(a, message)
{
try{
core.Assert.isNull(a);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @a {var} is an instance of @b {var} and registers
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isInstance : function(a, b, message)
{
try{
core.Assert.isInstance(a, b);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @a {var} is type of @b {var} and registers
* the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
isType : function(a, b, message)
{
try{
core.Assert.isType(a, b);
} catch(ex) {
return this.__failed(message, ex);
}
this.__passed(message);
},
/**
* Test whether @func {Function} raises an exception (which it should)
* and register the result to the internal storage. Optional @message {String?""}
* for more details to understand the context of the assertion.
*/
raisesException : function(func, message)
{
try {
func();
} catch(ex) {
return this.__passed(message);
}
this.__failed(message + ": Did not throwed an exception!");
},
/*
----------------------------------------------
STATUS API FOR TEST
----------------------------------------------
*/
/**
* Indicate that the test was successfully completed.
*/
done : function() {
this.__checkState();
},
/**
* Indicate that the test was not executed successfully
* e.g. exceptions or timeouts occured. Define the @message {String}
* to give more hints about the issue.
*/
failure : function(message)
{
if (this.__timeoutHandle) {
clearTimeout(this.__timeoutHandle);
}
this.__failureReason = "other";
this.__failureMessage = message;
this.__updateOnFatalError();
this.__suite.testFailed(this);
},
/*
----------------------------------------------
API FOR SUITE
----------------------------------------------
*/
/**
* {Map} Returns internal data to a Testem compatible JSON object.
*/
export : function()
{
// the result object to report for this test
return {
passed: this.__passedCount,
failed: this.__failedCount,
total: this.getTotalCount(),
id: this.getId(),
name: this.__title,
items: this.__items
};
},
/**
* {Boolean} Whether the test was successfully executed.
*/
wasSuccessful : function() {
return this.__failureReason == null;
},
/**
* {Boolean} Whether the test is running asynchronously
*/
isAsynchronous : function() {
return this.__timeout != null;
},
/**
* {Array} Returns all assertion results.
*/
getAssertions : function() {
return this.__items;
},
/**
* {Integer} Returns the number of assertions which are expected for being executed.
*/
getTotalCount : function() {
return this.__totalCount == null ? this.__passedCount + this.__failedCount : this.__totalCount;
},
/**
* {String} Returns a useful one liner of the status of the test
*/
getSummary : function()
{
var reason = this.__failureReason;
var message = this.__failureMessage;
var prefix = reason == null ? "Success" : "Failure"
var base = ": " + this.__title + " [" + core.Number.pad(this.__passedCount, 2) + "/" + core.Number.pad(this.getTotalCount(), 2) + "]";
var postfix = message == null ? reason == null ? "" : ": " + reason : ": " + message;
return prefix + base + postfix;
},
/**
* {String} Returns the reason of the failure. One of:
*
* - assertions: Failed assertions
* - mismatch: Mismatch in expexted vs. executed number of assertions
* - exception: Exception during execution
* - timeout: Timeout occoured
* - other: Custom error by test itself
*/
getFailureReason : function() {
return this.__failureReason;
},
/**
* {String} Returns the failure message (only when available).
*/
getFailureMessage : function() {
return this.__failureMessage;
},
/**
* {String} Returns the actual stack trace of the failure - if one happened.
*/
getFailureStack : function() {
return this.__failureStack;
},
/**
* {String} Returns the title of the test. This should be a human readable
* explanation of the issue to solve in the test case.
*/
getTitle : function() {
return this.__title;
},
/**
* Returns a unique ID for this test.
*/
getId : function() {
return core.util.Id.get(this);
},
/**
* Used to update failed count to sensible value when error happens
*/
__updateOnFatalError : function()
{
if (this.__totalCount == null)
{
// Assume that at least one assertion is failed
if (this.__failedCount == 0) {
this.__failedCount++;
}
}
else
{
// Mark all remaining assertions as failed
this.__failedCount = Math.max(this.__totalCount - this.__passedCount, 1);
}
},
/**
* This method is automatically triggered whenever the test was marked as done
*/
__checkState : function()
{
if (this.__timeoutHandle) {
clearTimeout(this.__timeoutHandle);
}
var failedAssertions = this.__failedCount;
if (failedAssertions)
{
this.__failureReason = "assertions";
this.__failureMessage = "Did not successfully pass " + failedAssertions + " assertions.";
this.__suite.testFailed(this);
}
else if (this.__totalCount != null && this.__totalCount != this.__passedCount)
{
this.__failureReason = "mismatch";
this.__failureMessage = "Did to match number of assertions expected (" + this.__totalCount + " vs. " + this.__passedCount + ")";
this.__updateOnFatalError();
this.__suite.testFailed(this);
}
else
{
this.__failureReason = null;
this.__failureMessage = null;
this.__suite.testPassed(this);
}
},
/**
* Executes the attached test method
*/
run : function()
{
// Asynchronous test with timeout
var timeout = this.__timeout;
if (timeout != null)
{
var self = this;
this.__timeoutHandle = setTimeout((function()
{
self.__failureReason = "timeout";
self.__failureMessage = "Timeout (" + timeout + "ms)";
self.__updateOnFatalError();
self.__suite.testFailed(self);
}), timeout);
}
try
{
this.__func();
}
catch(ex)
{
this.__failureReason = "exception";
this.__failureMessage = "Exception: " + ex;
this.__failureStack = ex.stack;
this.__updateOnFatalError();
this.__suite.testFailed(this);
return;
}
// Automatically mark as done for synchronous tests
if (timeout == null) {
this.__checkState();
}
}
}
});