Files
MyDIAPp/__test__/di/DependencyManager.test.ts
2025-10-28 20:29:46 -04:00

366 lines
10 KiB
TypeScript

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);
});
});
});