312 lines
10 KiB
JavaScript
312 lines
10 KiB
JavaScript
const Container = require('../../../src/application/services/Container');
|
|
|
|
describe('Container - Dependency Injection', () => {
|
|
let container;
|
|
|
|
beforeEach(() => {
|
|
container = new Container();
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
test('should initialize with empty Maps', () => {
|
|
expect(container.services).toBeInstanceOf(Map);
|
|
expect(container.factories).toBeInstanceOf(Map);
|
|
expect(container.lifetimes).toBeInstanceOf(Map);
|
|
expect(container.services.size).toBe(0);
|
|
expect(container.factories.size).toBe(0);
|
|
expect(container.lifetimes.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('register', () => {
|
|
test('should register a singleton service', () => {
|
|
const factory = jest.fn(() => ({ id: 1, name: 'Test Service' }));
|
|
|
|
container.register('TestService', factory, 'singleton');
|
|
|
|
expect(container.factories.has('TestService')).toBe(true);
|
|
expect(container.lifetimes.get('TestService')).toBe('singleton');
|
|
expect(factory).toHaveBeenCalled(); // Singleton is created immediately
|
|
expect(container.services.has('TestService')).toBe(true);
|
|
});
|
|
|
|
test('should register a transient service', () => {
|
|
const factory = jest.fn(() => ({ id: 2, name: 'Transient Service' }));
|
|
|
|
container.register('TransientService', factory, 'transient');
|
|
|
|
expect(container.factories.has('TransientService')).toBe(true);
|
|
expect(container.lifetimes.get('TransientService')).toBe('transient');
|
|
expect(factory).not.toHaveBeenCalled(); // Transient is NOT created immediately
|
|
expect(container.services.has('TransientService')).toBe(false);
|
|
});
|
|
|
|
test('should register a scoped service', () => {
|
|
const factory = jest.fn(() => ({ id: 3, name: 'Scoped Service' }));
|
|
|
|
container.register('ScopedService', factory, 'scoped');
|
|
|
|
expect(container.factories.has('ScopedService')).toBe(true);
|
|
expect(container.lifetimes.get('ScopedService')).toBe('scoped');
|
|
expect(factory).not.toHaveBeenCalled(); // Scoped is NOT created immediately
|
|
});
|
|
|
|
test('should default to singleton if lifetime not specified', () => {
|
|
const factory = () => ({ id: 4 });
|
|
|
|
container.register('DefaultService', factory);
|
|
|
|
expect(container.lifetimes.get('DefaultService')).toBe('singleton');
|
|
expect(container.services.has('DefaultService')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('resolve - singleton', () => {
|
|
test('should return the same instance for singleton', () => {
|
|
container.register('SingletonService', () => ({ id: Math.random() }), 'singleton');
|
|
|
|
const instance1 = container.resolve('SingletonService');
|
|
const instance2 = container.resolve('SingletonService');
|
|
|
|
expect(instance1).toBe(instance2);
|
|
expect(instance1.id).toBe(instance2.id);
|
|
});
|
|
|
|
test('should return the pre-created singleton instance', () => {
|
|
const mockInstance = { id: 123, name: 'Mock' };
|
|
container.register('PreCreatedService', () => mockInstance, 'singleton');
|
|
|
|
const resolved = container.resolve('PreCreatedService');
|
|
|
|
expect(resolved).toBe(mockInstance);
|
|
});
|
|
});
|
|
|
|
describe('resolve - transient', () => {
|
|
test('should return different instances for transient', () => {
|
|
container.register('TransientService', () => ({ id: Math.random() }), 'transient');
|
|
|
|
const instance1 = container.resolve('TransientService');
|
|
const instance2 = container.resolve('TransientService');
|
|
|
|
expect(instance1).not.toBe(instance2);
|
|
expect(instance1.id).not.toBe(instance2.id);
|
|
});
|
|
|
|
test('should call factory every time for transient', () => {
|
|
const factory = jest.fn(() => ({ id: Math.random() }));
|
|
container.register('TransientService', factory, 'transient');
|
|
|
|
container.resolve('TransientService');
|
|
container.resolve('TransientService');
|
|
container.resolve('TransientService');
|
|
|
|
expect(factory).toHaveBeenCalledTimes(3);
|
|
});
|
|
});
|
|
|
|
describe('resolve - scoped', () => {
|
|
test('should return same instance within the same scope', () => {
|
|
container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
|
|
|
|
const scope = container.createScope();
|
|
const instance1 = scope.resolve('ScopedService');
|
|
const instance2 = scope.resolve('ScopedService');
|
|
|
|
expect(instance1).toBe(instance2);
|
|
expect(instance1.id).toBe(instance2.id);
|
|
});
|
|
|
|
test('should return different instances for different scopes', () => {
|
|
container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
|
|
|
|
const scope1 = container.createScope();
|
|
const scope2 = container.createScope();
|
|
|
|
const instance1 = scope1.resolve('ScopedService');
|
|
const instance2 = scope2.resolve('ScopedService');
|
|
|
|
expect(instance1).not.toBe(instance2);
|
|
expect(instance1.id).not.toBe(instance2.id);
|
|
});
|
|
|
|
test('should resolve scoped service with scope parameter', () => {
|
|
const scopeMap = new Map();
|
|
container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
|
|
|
|
const instance1 = container.resolve('ScopedService', scopeMap);
|
|
const instance2 = container.resolve('ScopedService', scopeMap);
|
|
|
|
expect(instance1).toBe(instance2);
|
|
expect(scopeMap.has('ScopedService')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('resolve - error handling', () => {
|
|
test('should throw error for unregistered service', () => {
|
|
expect(() => container.resolve('NonExistentService')).toThrow(
|
|
"Service 'NonExistentService' is not registered"
|
|
);
|
|
});
|
|
|
|
test('should provide clear error message', () => {
|
|
try {
|
|
container.resolve('MissingService');
|
|
fail('Should have thrown error');
|
|
} catch (error) {
|
|
expect(error.message).toContain('MissingService');
|
|
expect(error.message).toContain('not registered');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('createScope', () => {
|
|
test('should create a scope with resolve method', () => {
|
|
const scope = container.createScope();
|
|
|
|
expect(scope).toHaveProperty('resolve');
|
|
expect(typeof scope.resolve).toBe('function');
|
|
});
|
|
|
|
test('should create independent scopes', () => {
|
|
container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
|
|
|
|
const scope1 = container.createScope();
|
|
const scope2 = container.createScope();
|
|
|
|
const instance1 = scope1.resolve('ScopedService');
|
|
const instance2 = scope2.resolve('ScopedService');
|
|
|
|
expect(instance1.id).not.toBe(instance2.id);
|
|
});
|
|
});
|
|
|
|
describe('real-world scenarios', () => {
|
|
test('should handle database connection as singleton', () => {
|
|
class DatabaseConnection {
|
|
constructor() {
|
|
this.id = Math.random();
|
|
this.connected = true;
|
|
}
|
|
}
|
|
|
|
container.register('Database', () => new DatabaseConnection(), 'singleton');
|
|
|
|
const db1 = container.resolve('Database');
|
|
const db2 = container.resolve('Database');
|
|
|
|
expect(db1).toBe(db2);
|
|
expect(db1.id).toBe(db2.id);
|
|
expect(db1.connected).toBe(true);
|
|
});
|
|
|
|
test('should handle logger as transient', () => {
|
|
class Logger {
|
|
constructor() {
|
|
this.id = Math.random();
|
|
}
|
|
log(msg) {
|
|
return `[${this.id}] ${msg}`;
|
|
}
|
|
}
|
|
|
|
container.register('Logger', () => new Logger(), 'transient');
|
|
|
|
const logger1 = container.resolve('Logger');
|
|
const logger2 = container.resolve('Logger');
|
|
|
|
expect(logger1).not.toBe(logger2);
|
|
expect(logger1.id).not.toBe(logger2.id);
|
|
});
|
|
|
|
test('should handle request context as scoped', () => {
|
|
class RequestContext {
|
|
constructor() {
|
|
this.requestId = Math.random();
|
|
this.user = null;
|
|
this.timestamp = Date.now();
|
|
}
|
|
}
|
|
|
|
container.register('RequestContext', () => new RequestContext(), 'scoped');
|
|
|
|
// Request 1
|
|
const request1Scope = container.createScope();
|
|
const ctx1a = request1Scope.resolve('RequestContext');
|
|
const ctx1b = request1Scope.resolve('RequestContext');
|
|
expect(ctx1a).toBe(ctx1b);
|
|
|
|
// Request 2
|
|
const request2Scope = container.createScope();
|
|
const ctx2 = request2Scope.resolve('RequestContext');
|
|
expect(ctx1a).not.toBe(ctx2);
|
|
expect(ctx1a.requestId).not.toBe(ctx2.requestId);
|
|
});
|
|
|
|
test('should handle dependency chain', () => {
|
|
class Repository {
|
|
constructor(db) {
|
|
this.db = db;
|
|
}
|
|
}
|
|
|
|
class Service {
|
|
constructor(repo) {
|
|
this.repo = repo;
|
|
}
|
|
}
|
|
|
|
const mockDb = { id: 1, connected: true };
|
|
container.register('Database', () => mockDb, 'singleton');
|
|
container.register('Repository', () => {
|
|
return new Repository(container.resolve('Database'));
|
|
}, 'singleton');
|
|
container.register('Service', () => {
|
|
return new Service(container.resolve('Repository'));
|
|
}, 'singleton');
|
|
|
|
const service = container.resolve('Service');
|
|
|
|
expect(service.repo).toBeDefined();
|
|
expect(service.repo.db).toBe(mockDb);
|
|
});
|
|
});
|
|
|
|
describe('mixed lifecycle scenarios', () => {
|
|
test('should handle mixed singleton and transient', () => {
|
|
container.register('Config', () => ({ port: 3000 }), 'singleton');
|
|
container.register('Handler', () => ({
|
|
id: Math.random(),
|
|
config: container.resolve('Config')
|
|
}), 'transient');
|
|
|
|
const handler1 = container.resolve('Handler');
|
|
const handler2 = container.resolve('Handler');
|
|
|
|
// Different handlers
|
|
expect(handler1).not.toBe(handler2);
|
|
expect(handler1.id).not.toBe(handler2.id);
|
|
|
|
// But same config
|
|
expect(handler1.config).toBe(handler2.config);
|
|
});
|
|
|
|
test('should handle mixed singleton and scoped', () => {
|
|
container.register('Database', () => ({ id: 'db' }), 'singleton');
|
|
container.register('RequestData', () => ({
|
|
id: Math.random()
|
|
}), 'scoped');
|
|
|
|
const scope1 = container.createScope();
|
|
const scope2 = container.createScope();
|
|
|
|
const data1a = scope1.resolve('RequestData');
|
|
const data1b = scope1.resolve('RequestData');
|
|
const data2 = scope2.resolve('RequestData');
|
|
|
|
expect(data1a).toBe(data1b); // Same within scope
|
|
expect(data1a).not.toBe(data2); // Different across scopes
|
|
});
|
|
});
|
|
});
|