universal-common-test
v1.0.6
Published
A lightweight unit testing library based on MSTest.
Downloads
25
Readme
universal-common-test
A lightweight unit testing library based on MSTest that provides a simple and flexible testing framework for JavaScript applications.
Installation
npm install universal-common-testOverview
This library provides a comprehensive testing framework with the following components:
TestEngine- Runs test classes and methods with full lifecycle supportTestResult- Encapsulates the result of a test executionAssert- Provides assertion methods using JavaScript's loose equality semanticsStrictAssert- Provides assertion methods using strict equality checksCollectionAssert- Specialized assertions for collections (arrays, sets, maps)AssertFailedError- Exception thrown when assertions fail
Usage
Importing
// 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";Basic Test Class
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");
}
}Running Tests
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
]);Using Assertions
Assert (Loose Equality)
// 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)
// 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
// 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");Async Test Methods
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);
}
}Test Results
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}`);Custom Test Runner
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;
}
}Testing with Mock Objects
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:
Class Level:
static classInitialize()- Run once before all testsstatic classCleanup()- Run once after all tests
Test Level:
testInitialize()- Run before each testtestCleanup()- 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
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
Parameterized Tests
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}`
);
}
}
}