petcarescript
Version:
PetCareScript - A modern, expressive programming language designed for humans
421 lines (350 loc) โข 11.4 kB
JavaScript
/**
* PetCareScript Testing Library
* TDD/BDD testing framework
*/
class TestSuite {
constructor(name) {
this.name = name;
this.tests = [];
this.beforeHooks = [];
this.afterHooks = [];
this.beforeEachHooks = [];
this.afterEachHooks = [];
this.passed = 0;
this.failed = 0;
this.skipped = 0;
}
addTest(name, fn, skip = false) {
this.tests.push({ name, fn, skip });
}
before(fn) {
this.beforeHooks.push(fn);
}
after(fn) {
this.afterHooks.push(fn);
}
beforeEach(fn) {
this.beforeEachHooks.push(fn);
}
afterEach(fn) {
this.afterEachHooks.push(fn);
}
async run() {
console.log(`\n๐งช Running test suite: ${this.name}`);
console.log('='.repeat(50));
// Run before hooks
for (const hook of this.beforeHooks) {
try {
await hook();
} catch (error) {
console.error(`โ Before hook failed: ${error.message}`);
return;
}
}
// Run tests
for (const test of this.tests) {
if (test.skip) {
console.log(`โญ๏ธ SKIPPED: ${test.name}`);
this.skipped++;
continue;
}
// Run beforeEach hooks
for (const hook of this.beforeEachHooks) {
try {
await hook();
} catch (error) {
console.error(`โ BeforeEach hook failed: ${error.message}`);
this.failed++;
continue;
}
}
try {
await test.fn();
console.log(`โ
PASSED: ${test.name}`);
this.passed++;
} catch (error) {
console.error(`โ FAILED: ${test.name}`);
console.error(` ${error.message}`);
this.failed++;
}
// Run afterEach hooks
for (const hook of this.afterEachHooks) {
try {
await hook();
} catch (error) {
console.error(`โ ๏ธ AfterEach hook failed: ${error.message}`);
}
}
}
// Run after hooks
for (const hook of this.afterHooks) {
try {
await hook();
} catch (error) {
console.error(`โ ๏ธ After hook failed: ${error.message}`);
}
}
// Print results
this.printResults();
}
printResults() {
console.log('\n๐ Test Results:');
console.log(` Passed: ${this.passed}`);
console.log(` Failed: ${this.failed}`);
console.log(` Skipped: ${this.skipped}`);
console.log(` Total: ${this.tests.length}`);
if (this.failed === 0) {
console.log('๐ All tests passed!');
} else {
console.log(`๐ฅ ${this.failed} test(s) failed`);
}
}
}
class TestRunner {
constructor() {
this.suites = [];
this.currentSuite = null;
}
describe(name, fn) {
const suite = new TestSuite(name);
this.suites.push(suite);
const previousSuite = this.currentSuite;
this.currentSuite = suite;
fn();
this.currentSuite = previousSuite;
return suite;
}
it(name, fn) {
if (!this.currentSuite) {
throw new Error('Test must be inside a describe block');
}
this.currentSuite.addTest(name, fn);
}
xit(name, fn) {
if (!this.currentSuite) {
throw new Error('Test must be inside a describe block');
}
this.currentSuite.addTest(name, fn, true); // skip = true
}
before(fn) {
if (!this.currentSuite) {
throw new Error('Hook must be inside a describe block');
}
this.currentSuite.before(fn);
}
after(fn) {
if (!this.currentSuite) {
throw new Error('Hook must be inside a describe block');
}
this.currentSuite.after(fn);
}
beforeEach(fn) {
if (!this.currentSuite) {
throw new Error('Hook must be inside a describe block');
}
this.currentSuite.beforeEach(fn);
}
afterEach(fn) {
if (!this.currentSuite) {
throw new Error('Hook must be inside a describe block');
}
this.currentSuite.afterEach(fn);
}
async run() {
console.log('๐งช Starting PetCareScript Test Runner');
let totalPassed = 0;
let totalFailed = 0;
let totalSkipped = 0;
for (const suite of this.suites) {
await suite.run();
totalPassed += suite.passed;
totalFailed += suite.failed;
totalSkipped += suite.skipped;
}
console.log('\n๐ Final Results:');
console.log(` Total Passed: ${totalPassed}`);
console.log(` Total Failed: ${totalFailed}`);
console.log(` Total Skipped: ${totalSkipped}`);
console.log(` Total Tests: ${totalPassed + totalFailed + totalSkipped}`);
return totalFailed === 0;
}
}
// Assertion library
class Assertion {
constructor(actual) {
this.actual = actual;
this.negated = false;
}
get not() {
this.negated = !this.negated;
return this;
}
toBe(expected) {
const result = this.actual === expected;
const shouldPass = this.negated ? !result : result;
if (!shouldPass) {
const message = this.negated
? `Expected ${this.actual} not to be ${expected}`
: `Expected ${this.actual} to be ${expected}`;
throw new Error(message);
}
return this;
}
toEqual(expected) {
const result = JSON.stringify(this.actual) === JSON.stringify(expected);
const shouldPass = this.negated ? !result : result;
if (!shouldPass) {
const message = this.negated
? `Expected ${JSON.stringify(this.actual)} not to equal ${JSON.stringify(expected)}`
: `Expected ${JSON.stringify(this.actual)} to equal ${JSON.stringify(expected)}`;
throw new Error(message);
}
return this;
}
toContain(expected) {
let result = false;
if (Array.isArray(this.actual)) {
result = this.actual.includes(expected);
} else if (typeof this.actual === 'string') {
result = this.actual.includes(expected);
}
const shouldPass = this.negated ? !result : result;
if (!shouldPass) {
const message = this.negated
? `Expected ${this.actual} not to contain ${expected}`
: `Expected ${this.actual} to contain ${expected}`;
throw new Error(message);
}
return this;
}
toBeTruthy() {
const result = !!this.actual;
const shouldPass = this.negated ? !result : result;
if (!shouldPass) {
const message = this.negated
? `Expected ${this.actual} not to be truthy`
: `Expected ${this.actual} to be truthy`;
throw new Error(message);
}
return this;
}
toBeFalsy() {
const result = !this.actual;
const shouldPass = this.negated ? !result : result;
if (!shouldPass) {
const message = this.negated
? `Expected ${this.actual} not to be falsy`
: `Expected ${this.actual} to be falsy`;
throw new Error(message);
}
return this;
}
toThrow(expectedError = null) {
if (typeof this.actual !== 'function') {
throw new Error('toThrow can only be used with functions');
}
let threw = false;
let error = null;
try {
this.actual();
} catch (e) {
threw = true;
error = e;
}
const shouldPass = this.negated ? !threw : threw;
if (!shouldPass) {
const message = this.negated
? `Expected function not to throw`
: `Expected function to throw`;
throw new Error(message);
}
if (expectedError && threw) {
if (typeof expectedError === 'string') {
if (!error.message.includes(expectedError)) {
throw new Error(`Expected error message to include "${expectedError}", got "${error.message}"`);
}
}
}
return this;
}
}
// Mock and spy functionality
class Mock {
constructor() {
this.calls = [];
this.returnValue = undefined;
this.implementation = null;
}
mockReturnValue(value) {
this.returnValue = value;
return this;
}
mockImplementation(fn) {
this.implementation = fn;
return this;
}
fn(...args) {
this.calls.push(args);
if (this.implementation) {
return this.implementation(...args);
}
return this.returnValue;
}
toHaveBeenCalled() {
return this.calls.length > 0;
}
toHaveBeenCalledWith(...args) {
return this.calls.some(call =>
JSON.stringify(call) === JSON.stringify(args)
);
}
toHaveBeenCalledTimes(times) {
return this.calls.length === times;
}
clear() {
this.calls = [];
this.returnValue = undefined;
this.implementation = null;
}
}
// Global test runner instance
const globalRunner = new TestRunner();
const TestingLib = {
// Test structure
describe: (name, fn) => globalRunner.describe(name, fn),
it: (name, fn) => globalRunner.it(name, fn),
xit: (name, fn) => globalRunner.xit(name, fn),
// Hooks
before: (fn) => globalRunner.before(fn),
after: (fn) => globalRunner.after(fn),
beforeEach: (fn) => globalRunner.beforeEach(fn),
afterEach: (fn) => globalRunner.afterEach(fn),
// Assertions
expect: (actual) => new Assertion(actual),
assert: (condition, message = 'Assertion failed') => {
if (!condition) {
throw new Error(message);
}
},
// Mocking
mock: () => new Mock(),
spy: (obj, method) => {
const original = obj[method];
const mock = new Mock();
obj[method] = (...args) => {
mock.fn(...args);
return original.apply(obj, args);
};
obj[method].mockClear = () => mock.clear();
obj[method].toHaveBeenCalled = () => mock.toHaveBeenCalled();
obj[method].toHaveBeenCalledWith = (...args) => mock.toHaveBeenCalledWith(...args);
obj[method].toHaveBeenCalledTimes = (times) => mock.toHaveBeenCalledTimes(times);
return obj[method];
},
// Test runner
run: () => globalRunner.run(),
// Utilities
createSuite: (name) => new TestSuite(name),
createRunner: () => new TestRunner()
};
module.exports = TestingLib;