A lightweight unit testing library based on MSTest.
npm install universal-common-test
npm install universal-common-test
`
Overview
This library provides a comprehensive testing framework with the following components:
- TestEngine - Runs test classes and methods with full lifecycle support
- TestResult - Encapsulates the result of a test execution
- Assert - Provides assertion methods using JavaScript's loose equality semantics
- StrictAssert - Provides assertion methods using strict equality checks
- CollectionAssert - Specialized assertions for collections (arrays, sets, maps)
- AssertFailedError - Exception thrown when assertions fail
Usage
$3
`javascript
// Import the test engine and result
import TestEngine from "universal-common-test/TestEngine.js";
import TestResult from "universal-common-test/TestResult.js";
// Import assertion utilities
import { Assert, StrictAssert, CollectionAssert, AssertFailedError } from "universal-common-test";
`
$3
`javascript
import { Assert } from "universal-common-test";
class CalculatorTests {
// Instance field for test state
calculator;
// Called once before all tests in the class
static classInitialize() {
console.log("Setting up test class");
}
// Called before each test method
testInitialize() {
this.calculator = new Calculator();
}
// Test methods - any public method not in the lifecycle
testAddition() {
const result = this.calculator.add(2, 3);
Assert.areEqual(5, result, "2 + 3 should equal 5");
}
testSubtraction() {
const result = this.calculator.subtract(10, 4);
Assert.areEqual(6, result);
}
// Called after each test method
testCleanup() {
this.calculator = null;
}
// Called once after all tests in the class
static classCleanup() {
console.log("Cleaning up test class");
}
}
`
$3
`javascript
const engine = new TestEngine();
// Run all tests in a class
const results = await engine.runClassAsync(CalculatorTests);
// Run a specific test method
const singleResult = await engine.runMethodAsync(CalculatorTests, "testAddition");
// Run multiple test classes
const allResults = await engine.runAllAsync([
CalculatorTests,
StringUtilsTests,
DatabaseTests
]);
`
$3
#### Assert (Loose Equality)
`javascript
// Basic assertions
Assert.areEqual({a: 1, b: 2}, {a: 1, b: 2}); // Deep equality
Assert.areEquivalent("5", 5); // Uses == comparison
Assert.areSame(obj1, obj1); // Reference equality
Assert.isTrue(condition);
Assert.isFalse(condition);
Assert.isNull(value);
Assert.isNotNull(value);
// Type checking
Assert.isInstanceOf("hello", String);
Assert.isInstanceOf(42, "number");
Assert.isInstanceOf(new Date(), Date);
// Error assertions
Assert.throwsError(TypeError, () => {
null.someMethod();
});
// Async error assertions
await Assert.throwsErrorAsync(ApiError, async () => {
await api.unauthorizedCall();
});
`
#### StrictAssert (Strict Equality)
`javascript
// Strict comparisons
StrictAssert.areEqual(5, 5); // Uses ===
StrictAssert.isTrue(true); // Must be exactly true
StrictAssert.isFalse(false); // Must be exactly false
StrictAssert.isNull(null); // Must be exactly null
StrictAssert.isUndefined(undefined); // Must be exactly undefined
// Type checking
StrictAssert.isTypeOf("hello", "string");
StrictAssert.isInstanceOf(new Map(), Map);
`
#### CollectionAssert
`javascript
// Array/collection assertions
CollectionAssert.areEqual([1, 2, 3], [1, 2, 3]); // Same order
CollectionAssert.areEquivalent([3, 1, 2], [1, 2, 3]); // Any order
CollectionAssert.contains([1, 2, 3], 2);
CollectionAssert.doesNotContain([1, 2, 3], 4);
// Set operations
CollectionAssert.isSubsetOf([1, 2], [1, 2, 3, 4]);
CollectionAssert.isNotSubsetOf([1, 5], [1, 2, 3, 4]);
// Works with any iterable
CollectionAssert.areEqual(new Set([1, 2, 3]), [1, 2, 3]);
CollectionAssert.contains("hello", "e");
`
$3
`javascript
class AsyncTests {
async testApiCall() {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
Assert.isNotNull(data);
Assert.areEqual(200, response.status);
}
async testDatabaseQuery() {
const users = await db.query("SELECT * FROM users");
CollectionAssert.contains(users, expectedUser);
}
}
`
$3
`javascript
const engine = new TestEngine();
const results = await engine.runClassAsync(MyTests);
for (const result of results) {
console.log(${result.className}.${result.testName}:);
console.log( Passed: ${result.passed});
console.log( Duration: ${result.duration}ms);
if (result.failed) {
console.error( Error: ${result.error.message});
console.error( Stack: ${result.error.stack});
}
}
// Summary
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => r.failed).length;
console.log(Total: ${results.length}, Passed: ${passed}, Failed: ${failed});
`
$3
`javascript
class TestRunner {
async runAllTests() {
const testClasses = [
UnitTests,
IntegrationTests,
PerformanceTests
];
const engine = new TestEngine();
const results = [];
for (const testClass of testClasses) {
console.log(Running ${testClass.name}...);
const classResults = await engine.runClassAsync(testClass);
results.push(...classResults);
// Report progress
const failedInClass = classResults.filter(r => r.failed);
if (failedInClass.length > 0) {
console.error( ${failedInClass.length} test(s) failed);
}
}
return results;
}
}
`
$3
`javascript
class ServiceTests {
mockRepository;
service;
testInitialize() {
// Create mock repository
this.mockRepository = {
findById: (id) => {
if (id === 1) return { id: 1, name: "Test User" };
return null;
},
save: (user) => {
return { ...user, id: Date.now() };
}
};
this.service = new UserService(this.mockRepository);
}
async testGetUser() {
const user = await this.service.getUser(1);
Assert.isNotNull(user);
Assert.areEqual("Test User", user.name);
}
async testGetNonExistentUser() {
await Assert.throwsErrorAsync(NotFoundError, async () => {
await this.service.getUser(999);
});
}
}
`
Test Lifecycle
The TestEngine supports a comprehensive lifecycle pattern:
1. Class Level:
- static classInitialize() - Run once before all tests
- static classCleanup() - Run once after all tests
2. Test Level:
- testInitialize() - Run before each test
- testCleanup() - Run after each test
All lifecycle methods can be synchronous or asynchronous. If any initialization method fails, subsequent tests will fail with the initialization error.
Error Handling
`javascript
class ErrorHandlingTests {
testSynchronousError() {
Assert.throwsError(TypeError, () => {
const obj = null;
obj.someMethod(); // Will throw TypeError
});
}
async testAsynchronousError() {
await Assert.throwsErrorAsync(NetworkError, async () => {
await fetchWithTimeout("https://slow.example.com", 100);
});
}
testCustomError() {
Assert.throwsError(AssertFailedError, () => {
Assert.areEqual(1, 2, "Numbers should be equal");
});
}
}
`
Advanced Usage
$3
`javascript
class ParameterizedTests {
testCases = [
{ input: [1, 2], expected: 3 },
{ input: [0, 0], expected: 0 },
{ input: [-1, 1], expected: 0 },
{ input: [10, -5], expected: 5 }
];
testAdditionWithMultipleInputs() {
const calculator = new Calculator();
for (const testCase of this.testCases) {
const [a, b] = testCase.input;
const result = calculator.add(a, b);
Assert.areEqual(
testCase.expected,
result,
Failed for inputs: ${a}, ${b}
);
}
}
}
``