Initial Commit
This commit is contained in:
365
__test__/di/DependencyManager.test.ts
Normal file
365
__test__/di/DependencyManager.test.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user