I also was looking for some solutions that would allow me to write simple tests without external libraries. This is especially useful when conducting interviews or being interviewed. I needed the test functions to be asynchronous and to be able to use done
method for callback or promise-based tests
Below is the example that I usually copy-paste to the file I want to quickly test.
type Config = {
logPerformance?: boolean;
timeout?: number; // Timeout in milliseconds
};
function createTestRunner(initialConfig?: Config) {
let globalConfig: Config = {
logPerformance: false,
timeout: 5000,
...initialConfig,
};
const assert = {
condition: (condition: boolean, message?: string) => {
if (!condition) {
throw new Error(message || "Assertion failed: condition is false");
}
},
isDeepEqual: (a: any, b: any, message?: string) => {
const stringify1 = JSON.stringify(a);
const stringify2 = JSON.stringify(b);
if (stringify1 !== stringify2) {
throw new Error(
message ||
`Assertion failed: values are not equal ${stringify1} !== ${stringify2}`
);
}
},
shouldThrow: (fn: Function) => {
const message = "Assertion failed: the function hasn't thrown";
try {
fn();
throw new Error(message);
} catch (e) {
if (e instanceof Error && e.message === message) {
throw e;
}
return true;
}
},
};
function setConfig(config: Config) {
globalConfig = { ...globalConfig, ...config };
}
function it(
desc: string,
fn: (done: (error?: any) => void) => void | Promise<void>,
config?: Config
) {
const { logPerformance, timeout } = { ...globalConfig, ...config };
const startTime = Date.now();
const testPromise = executeTestFunction(fn, timeout);
handleTestResult(testPromise, desc, startTime, logPerformance);
}
function executeTestFunction(fn: Function, timeout?: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
let doneCalled = false;
const done = (error?: any) => {
if (doneCalled) {
reject(new Error("done() called multiple times"));
return;
}
doneCalled = true;
if (error) {
reject(error);
} else {
resolve();
}
};
try {
const result = fn.length > 0 ? fn(done) : fn();
if (result instanceof Promise) {
result.then(resolve).catch(reject);
} else if (fn.length === 0 && result === undefined) {
// Synchronous test passed
resolve();
}
if (fn.length > 0 && result === undefined) {
const timeoutDuration = timeout ?? globalConfig.timeout ?? 5000;
setTimeout(() => {
if (!doneCalled) {
reject(new Error("Test timed out: done() was not called"));
}
}, timeoutDuration);
}
} catch (error) {
reject(error);
}
});
}
function handleTestResult(
testPromise: Promise<void>,
desc: string,
startTime: number,
logPerformance?: boolean
) {
testPromise
.then(() => {
logTestSuccess(desc, startTime, logPerformance);
})
.catch((error) => {
logTestFailure(desc, startTime, error, logPerformance);
});
}
function logTestSuccess(
desc: string,
startTime: number,
logPerformance?: boolean
) {
const endTime = Date.now();
let message = `\x1b[32m\u2714 ${desc}\x1b[0m`;
if (logPerformance) {
const duration = endTime - startTime;
message += ` (Duration: ${duration} ms)`;
}
console.log(message);
}
function logTestFailure(
desc: string,
startTime: number,
error: any,
logPerformance?: boolean
) {
const endTime = Date.now();
let message = `\n\x1b[31m\u2718 ${desc}\x1b[0m`;
if (logPerformance) {
const duration = endTime - startTime;
message += ` (Duration: ${duration} ms)`;
}
console.log(message);
console.error(error);
}
// Return the methods
return { it, assert, setConfig };
}
This is the Usage example
const { it, assert, setConfig } = createTestRunner({
logPerformance: true,
timeout: 5000,
});
// Synchronous test
it("should add numbers correctly", () => {
const result = 1 + 1;
assert.condition(result === 2, "1 + 1 should equal 2");
});
// Promise-based asynchronous test
it("should resolve after 1 second", () => {
return new Promise<void>((resolve) => {
setTimeout(() => {
assert.condition(true);
resolve();
}, 1000);
});
});
// Callback-based asynchronous test with custom timeout
it(
"should call done after async operation",
(done) => {
setTimeout(() => {
assert.condition(true);
done();
}, 3000);
},
{
timeout: 4000,
}
);
there is a gist that you can fork and use when needed