negyedik gyakorlat + megoldasok
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user