Last active
May 11, 2018 05:54
-
-
Save MarkKoz/8c776697e82f1596cef983683c218add to your computer and use it in GitHub Desktop.
Simple unit tester class using Node.js and TypeScript.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as assert from "assert"; | |
import * as util from "util"; | |
interface Test { | |
input: any[]; | |
expected: any; | |
} | |
export class Problem { | |
readonly name: string; | |
readonly tests: Test[]; | |
readonly enabled: boolean; | |
/** | |
* Creates a new testable problem. | |
* | |
* @param name The problem's name. | |
* @param tests The tests to perform on solutions. | |
* @param enabled true if testing is enabled; false otherwise. | |
*/ | |
public constructor(name: string, tests: Test[], enabled: boolean = true) { | |
this.name = name; | |
this.tests = tests; | |
this.enabled = enabled; | |
} | |
/** | |
* Tests a solution to the problem. | |
* | |
* @param callback The function for the solution. | |
* @param name The solution's name. Default is the callback's name. | |
* @param printStack If the stack trace of any exceptions should be | |
* printed. | |
*/ | |
public test(callback: (...args: any[]) => any, name?: string, printStack: boolean = false) { | |
if (!this.enabled) | |
return; | |
name = (typeof name !== "undefined") ? name : callback.name; | |
let failed = 0; | |
for (let t of this.tests) { | |
try { | |
// Lambda to preserve "this" context. Deep copy to prevent callbacks from mutating each other's args. | |
assert.deepStrictEqual((() => callback.apply(this, Problem.deepCopy(t.input)))(), t.expected); | |
} catch (e) { | |
++failed; | |
console.error(`[${this.name}] ${name}`); | |
console.error(` Input: ${Problem.inspect(t.input)}`); | |
console.error(` Expected: ${Problem.inspect(t.expected)}`); | |
if (e instanceof assert.AssertionError) { | |
console.error(` Actual: ${Problem.inspect(e.actual)}`); | |
} else if (e instanceof Error) { | |
if (printStack) | |
console.error(Problem.indent(e.stack)); | |
else | |
console.error(` ${e.name}: ${e.message}`); | |
} else { | |
console.error(e); | |
} | |
} | |
} | |
if (failed) | |
console.error(`[${this.name}] ${name} - Failed ${failed} out of ${this.tests.length} tests.`); | |
else | |
console.log(`[${this.name}] ${name} - Passed all tests!`); | |
} | |
/** | |
* Performs a deep copy on an object. | |
* | |
* @param obj The object to copy. | |
* @returns A copy of the object. | |
*/ | |
private static deepCopy(obj: any[] | object) : any[] | object { | |
let v, key; | |
let output : any[] | object = Array.isArray(obj) ? [] : {}; | |
for (key in obj) { | |
v = obj[key]; | |
output[key] = (typeof v === "object") ? this.deepCopy(v) : v; | |
} | |
return output; | |
} | |
/** | |
* Indents every line of a string with spaces. | |
* | |
* @param str The string to indent. | |
* @param size The amount of chars by which to indent. | |
* @returns The indented string. | |
*/ | |
private static indent(str: string, size: number = 4): string { | |
return str.replace(/^(?!\s*$)/mg, " ".repeat(size)); | |
} | |
/** | |
* Gets a string representation of an object. | |
* | |
* Determines if the string contains multiple lines. If it does, a newline | |
* is prepended and every line is indented. | |
* | |
* @param obj The string to check. | |
* @returns The string representation of the object. | |
*/ | |
private static inspect(obj: any): string { | |
const str = util.inspect(obj, false, null); | |
for (let count = -1, index = 0; index !== -1; count++, index = str.indexOf('\n', index + 1)) | |
if (count > 1) | |
return this.indent("\n" + str, 8); | |
return str; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example use:
Success Output
[A Problem] John Doe - Passed all tests!
Failure Output
Stack Trace Output