366 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|