micro-should
Advanced tools
Comparing version 0.4.0 to 0.5.0
@@ -1,11 +0,73 @@ | ||
type ShouldRunner = { | ||
(message: string, test: () => void | Promise<void>): void; | ||
only(message: string, test: () => void | Promise<void>): void; | ||
skip(message: string, test: () => void | Promise<void>): void; | ||
run(): void; | ||
}; | ||
declare const should: ShouldRunner; | ||
declare const it: ShouldRunner; | ||
export function describe(prefix: string, fn: () => void): void; | ||
export { should, it }; | ||
export default should; | ||
/*! micro-should - MIT License (c) 2019 Paul Miller (paulmillr.com) */ | ||
/** | ||
* Micro testing framework with familiar syntax for browsers, node and others. | ||
* Supports fast mode (parallel), quiet mode (dot reporter), tree structures, CLI self-run auto-detection. | ||
*/ | ||
export interface StackItem { | ||
message: string; | ||
test?: () => Promise<any> | any; | ||
skip?: boolean; | ||
only?: boolean; | ||
prefix?: string; | ||
childPrefix?: string; | ||
path?: StackItem[]; | ||
beforeEach?: () => Promise<void> | void; | ||
afterEach?: () => Promise<void> | void; | ||
children: StackItem[]; | ||
} | ||
export interface Options { | ||
PRINT_TREE: boolean; | ||
PRINT_MULTILINE: boolean; | ||
STOP_AT_ERROR: boolean; | ||
MSHOULD_QUIET: boolean; | ||
} | ||
export interface DescribeFunction { | ||
(message: string, testFunctions: () => Promise<any> | any): void; | ||
skip: (message: string, test: () => Promise<any> | any) => void; | ||
} | ||
export interface TestFunction { | ||
(message: string, test: () => Promise<any> | any): void; | ||
/** | ||
* Registers test for "only" queue. When the queue is not empty, | ||
* it would ignore all other tests. Is limited to just one registered test. | ||
*/ | ||
only: (message: string, test: () => Promise<any> | any) => void; | ||
/** Registers test, but skips it while running. Can be used instead of commenting out the code. */ | ||
skip: (message: string, test: () => Promise<any> | any) => void; | ||
/** | ||
* Runs all registered tests. | ||
* After run, allows to run new tests without duplication: old test queue is cleaned up. | ||
* @param forceSequential - when `true`, disables automatic parallelization even when MSHOULD_FAST=1. | ||
* @returns resolved promise, after all tests have finished | ||
*/ | ||
run: (forceSequential?: boolean) => Promise<number>; | ||
/** | ||
* Executes .run() when passed argument is equal to CLI-passed file name. | ||
* Consider a project with 3 test files: a.test.js, b.test.js, all.js. | ||
* all.js imports a.test.js and b.test.js. | ||
* User runs node a.test.js; then node all.js; | ||
* Writing `it.run()` everywhere would fail, because it would try to run same tests twice. | ||
* However, `it.runWhen(import.meta.url)` would succeed, because it detects whether | ||
* current file is launched from CLI and not imported. | ||
* @example | ||
* it.runWhen(import.meta.url) | ||
*/ | ||
runWhen: (importMetaUrl: string) => Promise<number | undefined>; | ||
/** Parallel version, using node:cluster. Auto-selected when env var MSHOULD_FAST=1 is set. */ | ||
runParallel: () => Promise<number>; | ||
opts: Options; | ||
} | ||
export type EmptyFn = () => Promise<void> | void; | ||
declare const describe: DescribeFunction; | ||
declare function beforeEach(fn: EmptyFn): void; | ||
declare function afterEach(fn: EmptyFn): void; | ||
/** | ||
* Registers test for future running. | ||
* Would not auto-run, needs `it.run()` to be called at some point. | ||
* See {@link TestFunction} for methods. | ||
* @param message test title | ||
* @param test function, may be async | ||
*/ | ||
declare const it: TestFunction; | ||
export { it, describe, beforeEach, afterEach, it as should }; | ||
export default it; |
527
index.js
@@ -1,4 +0,28 @@ | ||
const red = '\x1b[31m'; | ||
const green = '\x1b[32m'; | ||
const reset = '\x1b[0m'; | ||
/*! micro-should - MIT License (c) 2019 Paul Miller (paulmillr.com) */ | ||
/** | ||
* Micro testing framework with familiar syntax for browsers, node and others. | ||
* Supports fast mode (parallel), quiet mode (dot reporter), tree structures, CLI self-run auto-detection. | ||
*/ | ||
const stack = [{ message: '', children: [] }]; | ||
const errorLog = []; | ||
let onlyStack; | ||
let running = false; | ||
const isCli = 'process' in globalThis; | ||
const opts = { | ||
PRINT_TREE: true, | ||
PRINT_MULTILINE: true, | ||
STOP_AT_ERROR: true, | ||
MSHOULD_QUIET: isCli && process.env.MSHOULD_QUIET, | ||
}; | ||
function isQuiet() { | ||
return opts.MSHOULD_QUIET; | ||
} | ||
// String formatting utils | ||
const _c = String.fromCharCode(27); // x1b, control code for terminal colors | ||
const c = { | ||
// colors | ||
red: _c + '[31m', | ||
green: _c + '[32m', | ||
reset: _c + '[0m', | ||
}; | ||
// We can write 'pending' test name and then overwrite it with actual result by using ${up}. | ||
@@ -8,4 +32,3 @@ // However, if test prints something to STDOUT, last line would get removed. | ||
// But we already use node modules for parallel cases, so maybe worth investigating. | ||
const up = '\x1b[A'; | ||
// const up = _c + '[A'; | ||
const LEAF_N = '├─'; | ||
@@ -20,165 +43,355 @@ const LEAF_E = '│ '; | ||
// const LEAF_S = ' '; | ||
async function run(info, printTree = false, multiLine = false, stopAtError = true) { | ||
if (!printTree && multiLine) console.log(); | ||
let output = info.message; | ||
if (printTree && info.only) { | ||
for (const parent of info.path) console.log(`${parent.prefix}${parent.message}`); | ||
} | ||
const path = `${info.path.map((i) => i.message).join('/')}/`; | ||
// Skip is always single-line | ||
if (multiLine && !info.skip) { | ||
console.log(printTree ? `${info.prefix}${output}: ☆` : `☆ ${path}${output}:`); | ||
} else if (info.skip) { | ||
console.log(printTree ? `${info.prefix}${output} (skip)` : `☆ ${path}${output} (skip)`); | ||
return true; | ||
} | ||
const printResult = (color, symbol) => | ||
console.log( | ||
printTree | ||
? `${info.childPrefix}${color}${output}: ${symbol}${reset}` | ||
: `${color}${symbol} ${path}${output}${reset}` | ||
); | ||
try { | ||
let result = await info.test(); | ||
printResult(green, '✓'); | ||
return true; | ||
} catch (error) { | ||
printResult(red, '☓'); | ||
if (stopAtError) throw error; | ||
else console.log(`${red}ERROR:${reset}`, error); | ||
} | ||
// Colorize string for terminal. | ||
function color(colorName, title) { | ||
return isCli ? `${c[colorName]}${title}${c.reset}` : title.toString(); | ||
} | ||
async function runParallel(tasks, cb) { | ||
// node.js / common.js-only | ||
const os = require('os'); | ||
const cluster = require('cluster'); | ||
tasks = tasks.filter((i) => !!i.test); // Filter describe elements | ||
const clusterId = cluster && cluster.worker ? `W${cluster.worker.id}` : 'M'; | ||
let WORKERS = +process.env.WORKERS || os.cpus().length; | ||
// Workers | ||
if (!cluster.isMaster) { | ||
process.on('error', (err) => console.log('Error (child crashed?):', err)); | ||
let tasksDone = 0; | ||
for (let i = 0; i < tasks.length; i++) { | ||
if (cluster.worker.id - 1 !== i % WORKERS) continue; | ||
await cb(tasks[i], false, false); | ||
tasksDone++; | ||
function log(...args) { | ||
if (isQuiet()) | ||
return logQuiet(false); | ||
// @ts-ignore | ||
console.log(...args); | ||
} | ||
function logQuiet(fail = false) { | ||
if (fail) { | ||
process.stderr.write(color('red', '!')); | ||
} | ||
process.send({ name: 'parallelTests', worker: clusterId, tasksDone }); | ||
process.exit(); | ||
} | ||
// Master | ||
return await new Promise((resolve, reject) => { | ||
console.log(`Starting parallel tests with ${WORKERS} workers and ${tasks.length} tasks`); | ||
cluster.on('exit', (worker, code) => { | ||
if (!code) return; | ||
console.error( | ||
`${red}Worker W${worker.id} (pid: ${worker.process.pid}) died with code: ${code}${reset}` | ||
); | ||
reject(new Error('Test worker died in agony')); | ||
}); | ||
let tasksDone = 0; | ||
let workersDone = 0; | ||
for (let i = 0; i < WORKERS; i++) { | ||
const worker = cluster.fork(); | ||
worker.on('error', (err) => reject(err)); | ||
worker.on('message', (msg) => { | ||
if (!msg || msg.name !== 'parallelTests') return; | ||
workersDone++; | ||
tasksDone += msg.tasksDone; | ||
if (workersDone === WORKERS) { | ||
if (tasksDone !== tasks.length) reject(new Error('Not all tasks finished.')); | ||
else resolve(tasksDone); | ||
} | ||
}); | ||
else { | ||
process.stdout.write('.'); | ||
} | ||
}); | ||
} | ||
const stack = [{ children: [] }]; | ||
const stackTop = () => stack[stack.length - 1]; | ||
const stackPop = () => stack.pop(); | ||
const stackClean = () => { | ||
stack.splice(0, stack.length); | ||
stack.push({ children: [] }); | ||
}; | ||
const stackAdd = (info) => { | ||
const c = { ...info, children: [] }; | ||
stackTop().children.push(c); | ||
stack.push(c); | ||
}; | ||
function addToErrorLog(title = '', error) { | ||
errorLog.push(`${title} ${error?.stack ? error.stack : error}`); | ||
// @ts-ignore | ||
if (!isQuiet()) | ||
console.error(error); // loud = show error now. quiet = show in the end | ||
} | ||
function formatPrefix(depth, prefix, isLast) { | ||
if (depth === 0) return { prefix: '', childPrefix: '' }; | ||
return { | ||
prefix: `${prefix}${isLast ? LEAF_L : LEAF_N}`, | ||
childPrefix: `${prefix}${isLast ? LEAF_S : LEAF_E}`, | ||
}; | ||
if (depth === 0) | ||
return { prefix: '', childPrefix: '' }; | ||
return { | ||
prefix: `${prefix}${isLast ? LEAF_L : LEAF_N}`, | ||
childPrefix: `${prefix}${isLast ? LEAF_S : LEAF_E}`, | ||
}; | ||
} | ||
function tdiff(start) { | ||
const sec = Math.round((Date.now() - start) / 1000); | ||
return sec < 60 ? `${sec} sec` : `${Math.floor(sec / 60)} min ${sec % 60} sec`; | ||
} | ||
async function runTest(info, printTree = false, multiLine = false, stopAtError = true) { | ||
if (!printTree && multiLine) | ||
log(); | ||
let title = info.message; | ||
if (typeof info.test !== 'function') | ||
throw new Error('internal test error: invalid info.test'); | ||
let messages = []; | ||
let onlyLogsToPrint = []; | ||
let beforeEachFns = []; | ||
let afterEachFns = []; // will be reversed | ||
for (const parent of info.path) { | ||
messages.push(parent.message); | ||
if (printTree && info.only) | ||
onlyLogsToPrint.push(`${parent.prefix}${parent.message}`); | ||
if (parent.beforeEach) | ||
beforeEachFns.push(parent.beforeEach); | ||
if (parent.afterEach) | ||
afterEachFns.push(parent.afterEach); | ||
} | ||
afterEachFns.reverse(); | ||
if (onlyLogsToPrint.length) | ||
onlyLogsToPrint.forEach((l) => log(l)); | ||
const path = `${messages.join('/')}/`; | ||
const full = path + title; | ||
// Skip is always single-line | ||
if (multiLine && !info.skip) { | ||
log(printTree ? `${info.prefix}${title}: ☆` : `☆ ${full}:`); | ||
} | ||
else if (info.skip) { | ||
log(printTree ? `${info.prefix}${title} (skip)` : `☆ ${full} (skip)`); | ||
return true; | ||
} | ||
// variables influencing state / print output: | ||
// fail = true | false | ||
// quiet = true | false | ||
// printTree = true | false (true when fast mode) | ||
// stopAtError = true | false | ||
function logTaskDone(fail = false, suffix = '') { | ||
const symbol = fail ? '☓' : '✓'; | ||
const clr = fail ? 'red' : 'green'; | ||
const title_ = suffix ? [title, suffix].join('/') : title; | ||
const full_ = suffix ? [full, suffix].join('/') : full; | ||
log(printTree | ||
? `${info.childPrefix}` + color(clr, `${title_}: ${symbol}`) | ||
: color(clr, `${symbol} ${full_}`)); | ||
} | ||
// Emit | ||
function logErrorStack(suffix) { | ||
if (isQuiet()) { | ||
// when quiet, either stop & log trace; or log ! | ||
if (stopAtError) { | ||
// stop, log whole path and trace | ||
console.error(); | ||
console.error(color('red', `☓ ${full}/${suffix}`)); | ||
} | ||
else { | ||
// log !, continue | ||
logQuiet(true); | ||
} | ||
} | ||
else { | ||
// when loud, log (maybe formatted) tree structure | ||
logTaskDone(true, suffix); | ||
} | ||
} | ||
// Run beforeEach hooks from parent contexts | ||
for (const beforeFn of beforeEachFns) { | ||
try { | ||
await beforeFn(); | ||
} | ||
catch (cause) { | ||
logErrorStack('beforeEach'); | ||
// @ts-ignore | ||
if (stopAtError) | ||
throw cause; | ||
else | ||
addToErrorLog(`${full}/beforeEach`, cause); | ||
return false; | ||
} | ||
} | ||
// Run test task | ||
try { | ||
// possible to do let result = ... in the future to save test outputs | ||
await info.test(); | ||
} | ||
catch (cause) { | ||
logErrorStack(''); | ||
// @ts-ignore | ||
if (stopAtError) | ||
throw cause; | ||
else | ||
addToErrorLog(`${full}`, cause); | ||
return false; | ||
} | ||
// Run afterEach hooks from parent contexts (in reverse order) | ||
for (const afterFn of afterEachFns) { | ||
try { | ||
await afterFn(); | ||
} | ||
catch (cause) { | ||
logErrorStack('afterEach'); | ||
// @ts-ignore | ||
if (stopAtError) | ||
throw cause; | ||
else | ||
addToErrorLog(`${full}/afterEach`, cause); | ||
return false; | ||
} | ||
} | ||
logTaskDone(); | ||
return true; | ||
} | ||
function stackTop() { | ||
return stack[stack.length - 1]; | ||
} | ||
function stackPop() { | ||
return stack.pop(); | ||
} | ||
function stackAdd(info) { | ||
const c = { ...info, children: [] }; | ||
stackTop().children.push(c); | ||
stack.push(c); | ||
} | ||
function stackFlatten(elm) { | ||
const out = []; | ||
const walk = (elm, depth = 0, isLast = false, prevPrefix = '', path = []) => { | ||
const { prefix, childPrefix } = formatPrefix(depth, prevPrefix, isLast); | ||
const newElm = { ...elm, prefix, childPrefix, path }; | ||
out.push(newElm); | ||
path = path.concat([newElm]); // Save prefixes so we can print path in 'only' case | ||
for (let i = 0; i < elm.children.length; i++) | ||
walk(elm.children[i], depth + 1, i === elm.children.length - 1, childPrefix, path); | ||
}; | ||
// Skip root | ||
for (const child of elm.children) walk(child); | ||
return out; | ||
const out = []; | ||
const walk = (elm, depth = 0, isLast = false, prevPrefix = '', path = []) => { | ||
const { prefix, childPrefix } = formatPrefix(depth, prevPrefix, isLast); | ||
const newElm = { ...elm, prefix, childPrefix, path }; | ||
out.push(newElm); | ||
path = path.concat([newElm]); // Save prefixes so we can print path in 'only' case | ||
const chl = elm.children; | ||
for (let i = 0; i < chl.length; i++) | ||
walk(chl[i], depth + 1, i === chl.length - 1, childPrefix, path); | ||
}; | ||
// Skip root | ||
for (const child of elm.children) | ||
walk(child); | ||
return out; | ||
} | ||
function describe(message, fn) { | ||
stackAdd({ message }); | ||
fn(); // Run function in the context of current stack path | ||
stackPop(); | ||
const describe = (message, fn) => { | ||
stackAdd({ message }); | ||
fn(); // Run function in the context of current stack path | ||
stackPop(); | ||
}; | ||
function describeSkip(message, _fn) { | ||
stackAdd({ message, skip: true }); | ||
// fn(); | ||
stackPop(); | ||
} | ||
function enqueue(info) { | ||
stackAdd(info); | ||
stackPop(); // remove from stack since there are no children | ||
describe.skip = describeSkip; | ||
function beforeEach(fn) { | ||
stackTop().beforeEach = fn; | ||
} | ||
let only; | ||
const should = (message, test) => enqueue({ message, test }); | ||
should.stack = stack; | ||
should.queue = () => stackFlatten(stack[0]); | ||
should.consumeQueue = () => { | ||
let items = should.queue().slice(); | ||
if (only) items = items.filter((i) => i.test === only.test); | ||
stackClean(); // Remove all elements, so next call won't process them twice | ||
only = undefined; | ||
return items; | ||
}; | ||
should.only = (message, test) => enqueue((only = { message, test, only: true })); | ||
should.skip = (message, test) => enqueue({ message, test, skip: true }); | ||
should.PRINT_TREE = true; | ||
should.PRINT_MULTILINE = true; | ||
should.STOP_AT_ERROR = true; | ||
should.run = () => { | ||
const items = should.consumeQueue(); | ||
// Return promise, so we can wait before section is complete without breaking anything | ||
return (async () => { | ||
for (const test of items) { | ||
if (should.PRINT_TREE && !test.test) console.log(`${test.prefix}${test.message}`); | ||
if (!test.test) continue; | ||
await run(test, should.PRINT_TREE, should.PRINT_MULTILINE, should.STOP_AT_ERROR); | ||
function afterEach(fn) { | ||
stackTop().afterEach = fn; | ||
} | ||
function register(info) { | ||
stackAdd(info); | ||
stackPop(); // remove from stack since there are no children | ||
} | ||
function cloneAndReset() { | ||
let items = stackFlatten(stack[0]).slice(); | ||
if (onlyStack) | ||
items = items.filter((i) => i.test === onlyStack.test); | ||
stack.splice(0, stack.length); | ||
stack.push({ message: '', children: [] }); | ||
onlyStack = undefined; | ||
return items; | ||
} | ||
// 123 tests (+quiet +fast-x8) started... | ||
function begin(total, workers) { | ||
const features = [isQuiet() ? '+quiet' : '', workers ? `+fast-x${workers}` : ''].filter((a) => a); | ||
const modes = features.length ? `(${features.join(' ')}) ` : ''; | ||
// No need to log stats when tests fit on one screen | ||
if (total > 32) | ||
console.log(`${color('green', total.toString())} tests ${modes}started...`); | ||
} | ||
function finalize(total, startTime) { | ||
console.log(); | ||
if (isQuiet()) | ||
console.log(); | ||
const totalFailed = errorLog.length; | ||
if (totalFailed) { | ||
console.error(); | ||
console.error(`${color('red', totalFailed)} tests failed`); | ||
if (isQuiet()) { | ||
errorLog.forEach((err) => console.error(err)); | ||
} | ||
} | ||
})(); | ||
}; | ||
else { | ||
console.log(`${color('green', total)} tests passed in ${tdiff(startTime)}`); | ||
} | ||
} | ||
async function runTests(forceSequential = false) { | ||
if (running) | ||
throw new Error('should.run() has already been called, wait for end'); | ||
if (!forceSequential && isCli && !!process?.env?.MSHOULD_FAST) | ||
return runTestsInParallel(); | ||
running = true; | ||
const tasks = cloneAndReset(); | ||
const total = tasks.filter((i) => !!i.test).length; | ||
begin(total); | ||
const startTime = Date.now(); | ||
for (const test of tasks) { | ||
if (opts.PRINT_TREE && !test.test) | ||
log(`${test.prefix}${test.message}`); | ||
if (!test.test) | ||
continue; | ||
await runTest(test, opts.PRINT_TREE, opts.PRINT_MULTILINE, opts.STOP_AT_ERROR); | ||
} | ||
finalize(total, startTime); | ||
running = false; | ||
return total; | ||
} | ||
async function runTestsWhen(importMetaUrl) { | ||
if (!isCli) | ||
throw new Error('cannot be used outside of CLI'); | ||
// @ts-ignore | ||
const url = await import('node:url'); | ||
return importMetaUrl === url.pathToFileURL(process.argv[1]).href ? runTests() : undefined; | ||
} | ||
// Doesn't support tree and multiline | ||
should.runParallel = () => | ||
runParallel(should.consumeQueue(), (info) => run(info, false, false, should.STOP_AT_ERROR)); | ||
module.exports = { should, it: should, describe, default: should }; | ||
// TODO: support beforeEach, afterEach | ||
async function runTestsInParallel() { | ||
if (!isCli) | ||
throw new Error('must run in cli'); | ||
if ('deno' in process.versions) | ||
return runTests(true); | ||
const tasks = cloneAndReset().filter((i) => !!i.test); // Filter describe elements | ||
const total = tasks.length; | ||
const startTime = Date.now(); | ||
const runTestPar = (info) => runTest(info, false, false, opts.STOP_AT_ERROR); | ||
let cluster, err; | ||
let totalW = Number.parseInt(process.env.MSHOULD_FAST, 10); | ||
if (totalW === 1) | ||
totalW = 0; | ||
try { | ||
// @ts-ignore | ||
cluster = (await import('node:cluster')).default; | ||
// @ts-ignore | ||
if (!totalW) | ||
totalW = (await import('node:os')).cpus().length; | ||
} | ||
catch (error) { | ||
err = error; | ||
} | ||
if (!cluster || !totalW) | ||
throw new Error('parallel tests are not supported: ' + err); | ||
// the code is ran in workers | ||
if (!cluster.isPrimary) { | ||
process.on('error', (err) => console.log('internal error:', 'child crashed?', err)); | ||
let tasksDone = 0; | ||
const id = cluster.worker.id; | ||
const strId = 'W' + id; | ||
for (let i = 0; i < tasks.length; i++) { | ||
if (i % totalW !== id - 1) | ||
continue; | ||
await runTestPar(tasks[i]); | ||
tasksDone++; | ||
} | ||
process.send({ name: 'parallelTests', worker: strId, tasksDone, errorLog }); | ||
process.exit(); | ||
} | ||
// the code is ran in primary process | ||
return await new Promise((resolve, reject) => { | ||
begin(total, totalW); | ||
console.log(); | ||
const workers = []; | ||
let tasksDone = 0; | ||
let workersDone = 0; | ||
cluster.on('exit', (worker, code) => { | ||
if (!code) | ||
return; | ||
const msg = `Worker W${worker.id} (pid: ${worker.process.pid}) crashed with code: ${code}`; | ||
// @ts-ignore | ||
console.error(color('red', msg)); | ||
workers.forEach((w) => w.kill()); // Shutdown other workers | ||
reject(new Error(msg)); | ||
}); | ||
for (let i = 0; i < totalW; i++) { | ||
const worker = cluster.fork(); | ||
workers.push(worker); | ||
worker.on('error', (err) => reject(err)); | ||
worker.on('message', (msg) => { | ||
if (!msg || msg.name !== 'parallelTests') | ||
return; | ||
workersDone++; | ||
tasksDone += msg.tasksDone; | ||
msg.errorLog.forEach((item) => errorLog.push(item)); | ||
if (workersDone === totalW) { | ||
if (tasksDone === total) { | ||
finalize(total, startTime); | ||
resolve(tasksDone); | ||
} | ||
else { | ||
reject(new Error('internal error: not all tasks have been completed')); | ||
} | ||
} | ||
}); | ||
} | ||
}); | ||
} | ||
/** | ||
* Registers test for future running. | ||
* Would not auto-run, needs `it.run()` to be called at some point. | ||
* See {@link TestFunction} for methods. | ||
* @param message test title | ||
* @param test function, may be async | ||
*/ | ||
const it = (message, test) => register({ message, test, children: [] }); | ||
it.only = (message, test) => register((onlyStack = { message, test, children: [], only: true })); | ||
it.skip = (message, test) => register({ message, test, children: [], skip: true }); | ||
it.run = runTests; | ||
it.runWhen = runTestsWhen; | ||
it.runParallel = runTestsInParallel; | ||
it.opts = opts; | ||
export { it, describe, beforeEach, afterEach, it as should }; | ||
export default it; |
{ | ||
"name": "micro-should", | ||
"version": "0.4.0", | ||
"description": "Simplest zero-dependency testing framework, a drop-in replacement for Mocha", | ||
"version": "0.5.0", | ||
"description": "Micro testing framework with familiar syntax, multi-env ESM support & parallel execution", | ||
"type": "module", | ||
"module": "index.js", | ||
"main": "index.js", | ||
"files": [ | ||
"index.ts", | ||
"index.js", | ||
@@ -11,3 +14,6 @@ "index.d.ts" | ||
"scripts": { | ||
"test": "node tests/example.test.js && node tests/basic.test.js && node tests/parallel.test.js" | ||
"build": "tsc", | ||
"lint": "npx prettier index.ts test", | ||
"format": "npx prettier --write index.ts test", | ||
"test": "node test/basic.test.js; node test/example.test.js; node test/parallel.test.js" | ||
}, | ||
@@ -31,3 +37,8 @@ "homepage": "https://github.com/paulmillr/micro-should", | ||
"karma" | ||
] | ||
} | ||
], | ||
"devDependencies": { | ||
"@paulmillr/jsbt": "0.2.1", | ||
"prettier": "3.3.2", | ||
"typescript": "5.5.2" | ||
} | ||
} |
106
README.md
# micro-should | ||
Simplest zero-dependency testing framework, a drop-in replacement for Mocha. | ||
Micro testing framework with familiar syntax, multi-env ESM support & parallel execution. | ||
Supports async cases. Works with any assertion library. | ||
Used in [noble cryptography](https://paulmillr.com/noble) and [many other packages](https://github.com/paulmillr/micro-should/network/dependents). | ||
* `should(title, case)` (or `it(title, case)`) syntax | ||
## Usage | ||
> npm install micro-should | ||
> jsr add jsr:@paulmillr/micro-should | ||
Basic methods: | ||
* `should(title, case)` or `it(title, case)` syntax to register a test function | ||
* `should.run()` or `it.run()` must always be executed in the end | ||
ENV variables: | ||
* `MSHOULD_FAST=1` enables parallel execution in node.js and Bun. Values >1 will set worker count. | ||
* `MSHOULD_QUIET=1` enables "quiet" dot reporter | ||
Additional methods: | ||
* `describe(prefix, cases)` for nested execution | ||
* `beforeEacn(fn)` to execute code before a function in `describe` block | ||
* `afterEach` to execute code after a function in `describe` block | ||
* `should.only(title, case)` allows to limit tests to only one case | ||
* `should.skip(title, case)` allows to skip functions instead of commenting them out | ||
* `describe(prefix, cases)` for nested execution | ||
* `should.run()` must always be executed in the end | ||
* `should.runParallel()` for CPU-intensive tasks, would ramp up threads equal to CPU count | ||
* `describe.skip(prefix, cases)` to skip describe()-s | ||
* `should.runWhen(import.meta.url)` helper ensures CLI tests are not `run` twice if you're using many test files | ||
* Executes .run() when passed argument is equal to CLI-passed file name. | ||
Consider a project with 3 test files: a.test.js, b.test.js, all.js. all.js imports a.test.js and b.test.js. | ||
User runs node a.test.js; then node all.js; | ||
* Writing `it.run()` everywhere would fail, because it would try to run same tests twice. | ||
* However, `it.runWhen(import.meta.url)` would succeed, because it detects whether | ||
current file is launched from CLI and not imported. | ||
> npm install micro-should | ||
![](https://raw.githubusercontent.com/paulmillr/micro-should/e60028e947f3158c46314ef105b51b2a2948c025/screenshot.png) | ||
## Usage | ||
### Basic | ||
To run the example in parallel / quiet setting, save it as a.test.js: | ||
MSHOULD_FAST=1 MSHOULD_QUIET=1 node a.test.js | ||
```js | ||
const { should } = require('micro-should'); | ||
const assert = require('assert'); // You can use any assertion library (e.g. Chai or Expect.js), example uses built-in nodejs | ||
import { should } from 'micro-should'; | ||
import * as assert from 'node:assert'; // examples with node:assert | ||
// you can use any assertion library, e.g. Chai or Expect.js | ||
@@ -35,17 +63,12 @@ should('add two numbers together', () => { | ||
should('produce correct promise result', async () => { | ||
const fs = require('fs').promises; | ||
const fs = await import('node:fs/promises'); | ||
const data = await fs.readFile('README.md', 'utf-8'); | ||
assert.ok(data.includes('Minimal testing')); | ||
}); | ||
should.run(); | ||
``` | ||
// should.only("execute only one test", () => { | ||
// assert.ok(true); | ||
// }); | ||
### Nested | ||
// should.skip("disable one test by using skip", () => { | ||
// assert.ok(false); // would not execute | ||
// }) | ||
// Nested | ||
const { describe } = require('micro-should'); | ||
```js | ||
describe('during any time of day', () => { | ||
@@ -57,14 +80,49 @@ describe('without hesitation', () => { | ||
should('multiply three numbers together', () => { | ||
assert.equal(3 * 3 * 3, 27); | ||
should.skip("disable one test by using skip", () => { | ||
assert.ok(false); // would not execute | ||
}); | ||
// should.only("execute only one test", () => { | ||
// assert.ok(true); | ||
// }); | ||
}); | ||
}); | ||
// Execute this at the end of a file. | ||
should.run(); | ||
``` | ||
### Auto-run with cli, do not run when imported | ||
```js | ||
// a.test.js | ||
import { should } from 'micro-should'; | ||
should('2 + 2', () => { | ||
if (2 + 2 !== 4) throw new Error('invalid'); | ||
}); | ||
should.runWhen(import.meta.url); | ||
``` | ||
```js | ||
// b.test.js | ||
import * from './a.test.js'; | ||
should.runWhen(import.meta.url); | ||
``` | ||
### Options | ||
Options which can be set via command line, as environment variables: | ||
* `MSHOULD_FAST=1` enables parallel execution in node.js and Bun. Values >1 will set worker count. | ||
* `MSHOULD_QUIET=1` enables "quiet" dot reporter | ||
Options which can be set via code: | ||
```js | ||
import { should } from 'micro-should'; | ||
should.opts.STOP_AT_ERROR = false; // default=true | ||
should.opts.MSHOULD_QUIET = true; // same as env var | ||
``` | ||
## License | ||
MIT (c) Paul Miller (https://paulmillr.com), see LICENSE file. |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
37411
6
891
127
0
Yes
3
1