Files
GKNB_MSTM071/Backend/negyedik gyakorlat/tests/unit/application/Container.test.js
T
2026-03-04 20:02:39 +01:00

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