Update bun.lock and remove DependencyManager tests
- Added new dependency "@techniker-me/tools" and updated existing dependencies to specific versions in bun.lock - Removed the DependencyManager test file as part of codebase cleanup
This commit is contained in:
@@ -1,365 +0,0 @@
|
|||||||
import {describe, test, expect, beforeEach} from 'bun:test';
|
|
||||||
import {DependencyManager} from '../../src/di/DependencyManager';
|
|
||||||
|
|
||||||
// Test classes for dependency injection
|
|
||||||
class Logger {
|
|
||||||
log(message: string) {
|
|
||||||
return `LOG: ${message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Database {
|
|
||||||
constructor(private logger: Logger) {}
|
|
||||||
|
|
||||||
query(sql: string) {
|
|
||||||
this.logger.log(`Executing: ${sql}`);
|
|
||||||
return 'result';
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogger() {
|
|
||||||
return this.logger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserService {
|
|
||||||
constructor(
|
|
||||||
private db: Database,
|
|
||||||
private logger: Logger
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getUser(id: number) {
|
|
||||||
this.logger.log(`Getting user ${id}`);
|
|
||||||
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDb() {
|
|
||||||
return this.db;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogger() {
|
|
||||||
return this.logger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CircularA {
|
|
||||||
constructor(public b: CircularB) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CircularB {
|
|
||||||
constructor(public a: CircularA) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('DependencyManager', () => {
|
|
||||||
let dm: DependencyManager;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
dm = new DependencyManager();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('register', () => {
|
|
||||||
test('should register a class without dependencies', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should register a class with dependencies', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error when registering duplicate name', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
}).toThrow('Error: [Logger] is already registered');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error for invalid lifecycle', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger', Logger, [], 'invalid' as any);
|
|
||||||
}).toThrow('Invalid lifecycle [invalid]');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should accept valid lifecycle types', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger1', Logger, [], 'singleton');
|
|
||||||
}).not.toThrow();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger2', Logger, [], 'instance');
|
|
||||||
}).not.toThrow();
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.register('Logger3', Logger, [], 'service');
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('resolve', () => {
|
|
||||||
test('should resolve a simple class without dependencies', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
const logger = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger).toBeInstanceOf(Logger);
|
|
||||||
expect(logger.log('test')).toBe('LOG: test');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw error when resolving unregistered dependency', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('NonExistent');
|
|
||||||
}).toThrow('Error: Unable to resolve unregistered [NonExistent]');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolve class with dependencies', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
|
|
||||||
const db = dm.resolve<Database>('Database');
|
|
||||||
|
|
||||||
expect(db).toBeInstanceOf(Database);
|
|
||||||
expect(db.getLogger()).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolve nested dependencies', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
dm.register('UserService', UserService, ['Database', 'Logger']);
|
|
||||||
|
|
||||||
const userService = dm.resolve<UserService>('UserService');
|
|
||||||
|
|
||||||
expect(userService).toBeInstanceOf(UserService);
|
|
||||||
expect(userService.getDb()).toBeInstanceOf(Database);
|
|
||||||
expect(userService.getLogger()).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('singleton lifecycle', () => {
|
|
||||||
test('should return same instance for singleton lifecycle', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'singleton');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).toBe(logger2); // Same instance
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should inject same singleton instance into multiple dependents', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'singleton');
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
dm.register('UserService', UserService, ['Database', 'Logger']);
|
|
||||||
|
|
||||||
const userService = dm.resolve<UserService>('UserService');
|
|
||||||
const db = userService.getDb();
|
|
||||||
const loggerFromUserService = userService.getLogger();
|
|
||||||
const loggerFromDatabase = db.getLogger();
|
|
||||||
|
|
||||||
expect(loggerFromUserService).toBe(loggerFromDatabase); // Same singleton instance
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should use singleton by default', () => {
|
|
||||||
dm.register('Logger', Logger, []); // No lifecycle specified
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).toBe(logger2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('instance/service lifecycle (transient)', () => {
|
|
||||||
test('should return new instance each time for instance lifecycle', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'instance');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).not.toBe(logger2); // Different instances
|
|
||||||
expect(logger1).toBeInstanceOf(Logger);
|
|
||||||
expect(logger2).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return new instance each time for service lifecycle', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'service');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).not.toBe(logger2); // Different instances
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should inject new instances into each dependent', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'instance');
|
|
||||||
dm.register('Database', Database, ['Logger']);
|
|
||||||
dm.register('UserService', UserService, ['Database', 'Logger']);
|
|
||||||
|
|
||||||
const userService = dm.resolve<UserService>('UserService');
|
|
||||||
const loggerFromUserService = userService.getLogger();
|
|
||||||
const loggerFromDatabase = userService.getDb().getLogger();
|
|
||||||
|
|
||||||
expect(loggerFromUserService).not.toBe(loggerFromDatabase); // Different instances
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('circular dependency detection', () => {
|
|
||||||
test('should detect direct circular dependency', () => {
|
|
||||||
dm.register('CircularA', CircularA, ['CircularB']);
|
|
||||||
dm.register('CircularB', CircularB, ['CircularA']);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('CircularA');
|
|
||||||
}).toThrow('Circular dependency detected');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should detect self-referencing circular dependency', () => {
|
|
||||||
dm.register('SelfRef', Logger, ['SelfRef']);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('SelfRef');
|
|
||||||
}).toThrow('Circular dependency detected: [SelfRef]');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should detect indirect circular dependency chain', () => {
|
|
||||||
class A {
|
|
||||||
constructor(public b: any) {}
|
|
||||||
}
|
|
||||||
class B {
|
|
||||||
constructor(public c: any) {}
|
|
||||||
}
|
|
||||||
class C {
|
|
||||||
constructor(public a: any) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
dm.register('A', A, ['B']);
|
|
||||||
dm.register('B', B, ['C']);
|
|
||||||
dm.register('C', C, ['A']);
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('A');
|
|
||||||
}).toThrow('Circular dependency detected');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('has', () => {
|
|
||||||
test('should return true for registered dependency', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
expect(dm.has('Logger')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return false for unregistered dependency', () => {
|
|
||||||
expect(dm.has('NonExistent')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not throw error for unregistered dependency', () => {
|
|
||||||
expect(() => {
|
|
||||||
dm.has('NonExistent');
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('clearSingletons', () => {
|
|
||||||
test('should clear cached singleton instances', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'singleton');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
dm.clearSingletons();
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger1).not.toBe(logger2); // Different instances after clearing
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not affect registrations', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
dm.clearSingletons();
|
|
||||||
|
|
||||||
expect(dm.has('Logger')).toBe(true);
|
|
||||||
expect(() => {
|
|
||||||
dm.resolve('Logger');
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should work with multiple singletons', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'singleton');
|
|
||||||
dm.register('Database', Database, ['Logger'], 'singleton');
|
|
||||||
|
|
||||||
const logger1 = dm.resolve<Logger>('Logger');
|
|
||||||
const db1 = dm.resolve<Database>('Database');
|
|
||||||
|
|
||||||
dm.clearSingletons();
|
|
||||||
|
|
||||||
const logger2 = dm.resolve<Logger>('Logger');
|
|
||||||
const db2 = dm.resolve<Database>('Database');
|
|
||||||
|
|
||||||
expect(logger1).not.toBe(logger2);
|
|
||||||
expect(db1).not.toBe(db2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('complex scenarios', () => {
|
|
||||||
test('should handle mixed lifecycles', () => {
|
|
||||||
dm.register('Logger', Logger, [], 'instance'); // Transient
|
|
||||||
dm.register('Database', Database, ['Logger'], 'singleton');
|
|
||||||
dm.register('UserService', UserService, ['Database', 'Logger']);
|
|
||||||
|
|
||||||
const userService1 = dm.resolve<UserService>('UserService');
|
|
||||||
const userService2 = dm.resolve<UserService>('UserService');
|
|
||||||
|
|
||||||
// UserService is singleton by default
|
|
||||||
expect(userService1).toBe(userService2);
|
|
||||||
|
|
||||||
// Database should be same singleton
|
|
||||||
expect(userService1.getDb()).toBe(userService2.getDb());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle multiple dependencies of same type', () => {
|
|
||||||
class MultiDependent {
|
|
||||||
constructor(
|
|
||||||
public logger1: Logger,
|
|
||||||
public logger2: Logger
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
dm.register('Logger', Logger, [], 'instance');
|
|
||||||
dm.register('MultiDependent', MultiDependent, ['Logger', 'Logger']);
|
|
||||||
|
|
||||||
const multiDep = dm.resolve<MultiDependent>('MultiDependent');
|
|
||||||
|
|
||||||
// Should be different instances since Logger is transient
|
|
||||||
expect(multiDep.logger1).not.toBe(multiDep.logger2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle no dependencies (empty array)', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
const logger = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
expect(logger).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('type safety', () => {
|
|
||||||
test('should support generic type parameter', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
// TypeScript should infer the correct type
|
|
||||||
const logger = dm.resolve<Logger>('Logger');
|
|
||||||
|
|
||||||
// This should compile and work
|
|
||||||
expect(logger.log('test')).toBe('LOG: test');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should work without explicit type parameter', () => {
|
|
||||||
dm.register('Logger', Logger, []);
|
|
||||||
|
|
||||||
const logger = dm.resolve('Logger');
|
|
||||||
|
|
||||||
expect(logger).toBeInstanceOf(Logger);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
58
__test__/di/WhenManagingDependencies.test.ts
Normal file
58
__test__/di/WhenManagingDependencies.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||||
|
import { DependencyManager } from '../../src/di/DependencyManager';
|
||||||
|
import { Logger, Database, UserService } from '../mocks';
|
||||||
|
|
||||||
|
describe('When Managing Dependencies', () => {
|
||||||
|
let dependencyManager: DependencyManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dependencyManager = new DependencyManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GIVEN classes with no dependencies are registered with the dependency manager', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dependencyManager.register('Logger', Logger, [], 'transient');
|
||||||
|
dependencyManager.register('Database', Database, [], 'transient');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the class', () => {
|
||||||
|
const logger = dependencyManager.resolve<Logger>('Logger');
|
||||||
|
|
||||||
|
expect(logger).toBeDefined();
|
||||||
|
expect(logger).toBeInstanceOf(Logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GIVEN the class is registered a second time', () => {
|
||||||
|
it('throws an error', () => {
|
||||||
|
expect(() => dependencyManager.register('Logger', Logger, [])).toThrow('Error: [Logger] is already registered');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GIVEN a singleton class is registered with the dependency manager', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dependencyManager.register('Logger', Logger, [], 'singleton');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the class', () => {
|
||||||
|
const logger = dependencyManager.resolve<Logger>('Logger');
|
||||||
|
|
||||||
|
expect(logger).toBeDefined();
|
||||||
|
expect(logger).toBeInstanceOf(Logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GIVEN a UserService with Logger dependency is registered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
dependencyManager.register('UserService', UserService, ['Logger'], 'singleton');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the same instance when requested multiple times', () => {
|
||||||
|
const userService = dependencyManager.resolve<UserService>('UserService');
|
||||||
|
const userService2 = dependencyManager.resolve<UserService>('UserService');
|
||||||
|
|
||||||
|
expect(userService).toBe(userService2);
|
||||||
|
expect(userService2).toBe(userService);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
15
__test__/mocks/MockDatabase.ts
Normal file
15
__test__/mocks/MockDatabase.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type {Logger} from './MockLogger';
|
||||||
|
|
||||||
|
export class Database {
|
||||||
|
private _logger: Logger;
|
||||||
|
|
||||||
|
constructor(logger: Logger) {
|
||||||
|
this._logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public query(sql: string) {
|
||||||
|
this._logger.debug(`Executing: ${sql}`);
|
||||||
|
|
||||||
|
return 'result';
|
||||||
|
}
|
||||||
|
}
|
||||||
31
__test__/mocks/MockLogger.ts
Normal file
31
__test__/mocks/MockLogger.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export class Logger {
|
||||||
|
private _category: string;
|
||||||
|
|
||||||
|
constructor(category: string) {
|
||||||
|
this._category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public info(message: string, ...optionalParams: any[]) {
|
||||||
|
console.info(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public debug(message: string, ...optionalParams: any[]) {
|
||||||
|
console.debug(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(message: string, ...optionalParams: any[]) {
|
||||||
|
console.error(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public warn(message: string, ...optionalParams: any[]) {
|
||||||
|
console.warn(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public trace(message: string, ...optionalParams: any[]) {
|
||||||
|
console.trace(this.formatMessage(message), ...optionalParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMessage(message: string) {
|
||||||
|
return `${new Date().toISOString()} [${this._category}] ${message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
__test__/mocks/MockUserServices.ts
Normal file
14
__test__/mocks/MockUserServices.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type {Logger} from './MockLogger';
|
||||||
|
import type {Database} from './MockDatabase';
|
||||||
|
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
private db: Database,
|
||||||
|
private logger: Logger
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public getUser(id: number) {
|
||||||
|
this.logger.debug(`Getting user ${id}`);
|
||||||
|
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
__test__/mocks/index.ts
Normal file
3
__test__/mocks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './MockDatabase';
|
||||||
|
export * from './MockLogger';
|
||||||
|
export * from './MockUserServices';
|
||||||
11
src/Defaults.ts
Normal file
11
src/Defaults.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { LoggingLevel } from './logger/LoggingLevel';
|
||||||
|
|
||||||
|
export default class Defaults {
|
||||||
|
static get loggingLevel(): LoggingLevel {
|
||||||
|
return LoggingLevel.Info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
throw new Error('Defaults is a static class that may not be instantiated');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user