Skip to content

Instantly share code, notes, and snippets.

@captain-kark
Created November 20, 2020 19:45
Show Gist options
  • Save captain-kark/c8e06d50080e10cb30652bb43e32937e to your computer and use it in GitHub Desktop.
Save captain-kark/c8e06d50080e10cb30652bb43e32937e to your computer and use it in GitHub Desktop.
microtester.js
{
"name": "microtester",
"version": "0.1.0",
"description": "Zero-deps unit testing. Why is this not in the standard library?",
"scripts": {
"test": "./tests.js"
},
"author": "Andrew Yurisich",
"license": "MIT",
"dependencies": {},
"devDependencies": {}
}
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const colors = {
reset: '\033[0m',
red: '\033[31m',
green: '\033[32m',
yellow: '\033[33m',
};
const color = (color, text) => `${color}${text}${colors.reset}`;
const style = {
green: (text) => color(colors.green, text),
red: (text) => color(colors.red, text),
yellow: (text) => color(colors.yellow, text),
diff: (text) => text.replace(/(\s+\+)\s/g, style.green('$1 ')).replace(/(\s+\-)\s/g, style.red('$1 ')),
reset: () => colors.reset,
};
class Test {
constructor(testName, module) {
this.name = testName;
this.module = module;
this.result = {};
};
assert(expected, actual, typeCast=Object.toString) {
const [expectedRaw, actualRaw] = [expected, actual];
let failureMessage;
[expected, actual, failureMessage] = {
[Object.toString.toString()]: [expected, actual, failureMessageDefault], // others will alias this 'do nothing' default
[Number.toString()]: typeCast[Object.toString], // for example, numbers already compare just fine
[Array.toString()]: [expected.sort().join(' '), actual.sort().join(' '), failureMessageArray],
}[typeCast]
if (expected !== actual) {
this.result.message = style.diff(failureMessage(expectedRaw, actualRaw, this.name, this.module));
this.result.code = 1;
return this.done();
}
this.result.code = 0;
this.result.message = `${style.green('PASS')}: [ ${style.yellow(this.module)} ] ${this.name}`;
return this.done();
};
done() {
return this.result;
};
}
class Module {
constructor(testModuleFilename) {
this.testModule = require(path.resolve(testModuleFilename));
this.name = path.basename(testModuleFilename);
this.collected = this.testModule;
this.tests = [];
this.result = {};
}
assert() {
console.log(`START [ ${style.yellow(this.name)} ]`);
Object.entries(this.collected).forEach(collected => {
const [testName, testFn] = collected;
const t = new Test(testName, this.name);
testFn(t);
this.tests.push(t);
console.log(t.result.message);
});
return this.done();
}
done() {
const total = this.tests.length;
const failing = this.tests.reduce((failures, test) => {
const increase = test.result.code > 0 ? 1 : 0;
failures += increase;
return failures;
}, 0);
const failed = failing > 0 ? ` ${style.red(failing)} failing` : '';
const module = failing > 0 ? style.red(this.name) : style.green(this.name);
console.log(
`DONE: [ ${module} ]${failed}`
);
this.result.code = failing === 0 ? 0 : 1;
return this.result;
}
}
class Runner {
constructor(directoriesOrFiles) {
this.directoriesOrFiles = directoriesOrFiles;
this.directories = [];
this.files = [];
this.codes = [];
this.modules = [];
this.testFilePattern = /^test[A-Z0-9]{1}\w+\.js$/;
}
collect() {
const _collect = (directoryOrFile, results) => {
const dirOrFile = path.resolve(directoryOrFile);
if (!fs.existsSync(dirOrFile)) {
return;
}
if (fs.statSync(dirOrFile).isFile()) {
const file = dirOrFile;
if (path.basename(file).match(this.testFilePattern) && this.files.indexOf(file) < 0) {
this.files.push(file);
}
return;
}
fs.readdirSync(dirOrFile).forEach(df => {
if (fs.statSync(df).isFile()) {
_collect(df);
return;
}
const dir = fs.statSync(df).isDirectory() ? path.resolve(df) : undefined;
if (dir !== undefined && this.directories.indexOf(dir) < 0) {
this.directories.push(dir);
this.directoriesOrFiles.push(dir);
}
});
};
if (this.directoriesOrFiles === undefined || this.directoriesOrFiles.length === 0) {
this.files = fs.readdirSync('.').filter(f => fs.statSync(f).isFile() && f.match(this.testFilePattern));
return this;
}
let df;
while (df = this.directoriesOrFiles.shift()) {
_collect(df, { files: this.files, directories: this.directories });
};
return this;
}
assert() {
this.files.forEach(f => {
const code = new Module(f).assert().code;
if (process.argv.length <= 2 && code > 0) {
process.exit(code);
}
this.codes.push(code);
});
}
}
const failureMessageHeader = (name, module) => `${style.red('FAIL')}: [ ${style.red(module)} ] ${name}`;
const failureMessageDefault = ((expected, actual, name, module) => [
failureMessageHeader(name, module),
`+ Expected`,
`- Actual`,
``,
`+ ${expected}`,
`=`,
`- ${actual}`,
].join('\n'));
const zip = (a, b) => Array(Math.max(b.length, a.length)).fill().map((_,i) => [a[i], b[i]]);
const failureMessageArray = ((expectedRaw, actualRaw, name, module) => {
const pad = Math.max(...expectedRaw.map(e => e.length), 12) + 2;
return [
failureMessageHeader(name, module),
`+ Expected ${new Array(Math.max(pad - 12, 1)).fill(' ').join('')} - Actual`,
``,
...zip(expectedRaw, actualRaw).map(e => {
let [expected, actual] = e;
const expectedLength = expected === undefined ? ('undefined').length : expected.length;
const padExpected = new Array(pad - expectedLength).fill(' ').join('');
expected = expected === undefined ? style.yellow(expected) : expected;
actual = actual === undefined ? style.yellow(actual) : actual;
if (expected !== actual) {
expected = style.green(expected);
actual = style.red(actual);
}
return ` ${expected}${padExpected}${actual}`;
}),
].join('\n')
});
if (require.main === module) {
const testPaths = process.argv.slice(2);
new Runner(testPaths).collect().assert();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment