From 190f0c40a3669d3d78fad6d2f4d22291c2c72575 Mon Sep 17 00:00:00 2001 From: changchanghwang Date: Sun, 22 Mar 2026 16:32:07 +0900 Subject: [PATCH 1/4] feat: container reset with static method --- src/container/container.ts | 6 ++++++ test/container/container.test.ts | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/container/container.ts b/src/container/container.ts index 126f1ac..e25ac22 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -131,4 +131,10 @@ export class Container { private isDefault() { return this === ContainerRegistry.defaultContainer; } + + static reset(containerId: ContainerIdentifier, options?: { strategy?: 'value' | 'service' }) { + const container = ContainerRegistry.getContainer(containerId); + + container?.reset(options?.strategy); + } } diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 8f8ac19..81f4a93 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -136,6 +136,23 @@ describe('Container', () => { expect(second).not.toBe(Container.of().get(ResettableService)); }); + test('reset specific container with static method', () => { + class ResettableService {} + + Container.of('container').set({ + id: ResettableService, + Class: ResettableService, + name: 'ResettableService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); + + Container.reset('container', { strategy: 'service' }); + + expect(() => ContainerRegistry.getContainer('container')?.get(ResettableService)).toThrow(ServiceNotFoundError); + }); + test('has only reports local registrations', () => { const requestContainer = Container.of('container-local-has'); From 80f036a1efacb7dc669f402907de56f90e7d1c32 Mon Sep 17 00:00:00 2001 From: changchanghwang Date: Sun, 22 Mar 2026 17:08:56 +0900 Subject: [PATCH 2/4] refactor: rename method --- src/container/container.ts | 4 ++-- src/decorators/service.ts | 2 +- test/container/container.test.ts | 24 ++++++++++++------------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index e25ac22..9f0c863 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -30,9 +30,9 @@ export class Container { return container; } - public set(metadata: Metadata) { + public register(metadata: Metadata) { if (metadata.scope === 'singleton' && !this.isDefault()) { - ContainerRegistry.defaultContainer.set(metadata); + ContainerRegistry.defaultContainer.register(metadata); this.metadataMap.delete(metadata.id); return; } diff --git a/src/decorators/service.ts b/src/decorators/service.ts index 0ed207e..c3c62a1 100644 --- a/src/decorators/service.ts +++ b/src/decorators/service.ts @@ -23,7 +23,7 @@ export function Service(idOrOptions?: ServiceIdentifier | ServiceOption) { const injections = (context.metadata[INJECTION_KEY] ?? []) as InjectionMetadata[]; - ContainerRegistry.defaultContainer.set({ + ContainerRegistry.defaultContainer.register({ id: options?.id ?? target, Class: target, name: context.name, diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 81f4a93..b997073 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -34,7 +34,7 @@ describe('Container', () => { class RequestService {} - Container.of().set({ + Container.of().register({ id: RequestService, Class: RequestService, name: 'RequestService', @@ -58,7 +58,7 @@ describe('Container', () => { class SingletonService {} - requestContainer.set({ + requestContainer.register({ id: SingletonService, Class: SingletonService, name: 'SingletonService', @@ -75,7 +75,7 @@ describe('Container', () => { test('reset value clears cached instances but keeps registrations', () => { class ResettableService {} - Container.of().set({ + Container.of().register({ id: ResettableService, Class: ResettableService, name: 'ResettableService', @@ -97,7 +97,7 @@ describe('Container', () => { test('reset service removes registrations from the current container', () => { class ResettableService {} - Container.of().set({ + Container.of().register({ id: ResettableService, Class: ResettableService, name: 'ResettableService', @@ -116,7 +116,7 @@ describe('Container', () => { class ResettableService {} - Container.of().set({ + Container.of().register({ id: ResettableService, Class: ResettableService, name: 'ResettableService', @@ -139,7 +139,7 @@ describe('Container', () => { test('reset specific container with static method', () => { class ResettableService {} - Container.of('container').set({ + Container.of('container').register({ id: ResettableService, Class: ResettableService, name: 'ResettableService', @@ -158,7 +158,7 @@ describe('Container', () => { class ScopedService {} - Container.of().set({ + Container.of().register({ id: ScopedService, Class: ScopedService, name: 'ScopedService', @@ -190,7 +190,7 @@ describe('Container', () => { public alpha!: AlphaService; } - Container.of().set({ + Container.of().register({ id: AlphaService, Class: AlphaService, name: 'AlphaService', @@ -199,7 +199,7 @@ describe('Container', () => { value: EMPTY_VALUE, }); - Container.of().set({ + Container.of().register({ id: BetaService, Class: BetaService, name: 'BetaService', @@ -224,7 +224,7 @@ describe('Container', () => { class HealthyService {} - requestContainer.set({ + requestContainer.register({ id: AlphaService, Class: AlphaService, name: 'AlphaService', @@ -233,7 +233,7 @@ describe('Container', () => { value: EMPTY_VALUE, }); - requestContainer.set({ + requestContainer.register({ id: BetaService, Class: BetaService, name: 'BetaService', @@ -242,7 +242,7 @@ describe('Container', () => { value: EMPTY_VALUE, }); - requestContainer.set({ + requestContainer.register({ id: HealthyService, Class: HealthyService, name: 'HealthyService', From 8872337b518552731cbcf9a565a86322208dc620 Mon Sep 17 00:00:00 2001 From: changchanghwang Date: Sun, 22 Mar 2026 18:19:44 +0900 Subject: [PATCH 3/4] feat: user setted value in binding map --- src/container/container.ts | 27 ++++++++- test/container/container.test.ts | 95 ++++++++++++++++++++++++++++---- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index 9f0c863..dddd68d 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -7,6 +7,7 @@ export class Container { public readonly id: ContainerIdentifier; private metadataMap: Map = new Map(); + private bindingMap: Map = new Map(); private resolving = new Set(); private resolvingPath: ServiceIdentifier[] = []; @@ -34,7 +35,7 @@ export class Container { if (metadata.scope === 'singleton' && !this.isDefault()) { ContainerRegistry.defaultContainer.register(metadata); this.metadataMap.delete(metadata.id); - return; + return this; } const newMetadata: Metadata = { @@ -48,23 +49,45 @@ export class Container { } else { this.metadataMap.set(newMetadata.id, newMetadata); } + + return this; } public has(id: ServiceIdentifier): boolean { return this.metadataMap.has(id); } + public set(id: ServiceIdentifier, value: T) { + this.bindingMap.set(id, value); + return this; + } + + public remove(id: ServiceIdentifier) { + this.bindingMap.delete(id); + this.metadataMap.delete(id); + return this; + } + public reset(strategy: 'value' | 'service' = 'value') { if (strategy === 'value') { this.metadataMap.forEach((metadata) => { metadata.value = EMPTY_VALUE; }); + + return this; } else { + this.bindingMap.clear(); this.metadataMap.clear(); + + return this; } } public get(id: ServiceIdentifier): T { + if (this.bindingMap.has(id)) { + return this.bindingMap.get(id) as T; + } + let metadata = this.metadataMap.get(id); if (!metadata && !this.isDefault()) { @@ -132,7 +155,7 @@ export class Container { return this === ContainerRegistry.defaultContainer; } - static reset(containerId: ContainerIdentifier, options?: { strategy?: 'value' | 'service' }) { + public static reset(containerId: ContainerIdentifier, options?: { strategy?: 'value' | 'service' }) { const container = ContainerRegistry.getContainer(containerId); container?.reset(options?.strategy); diff --git a/test/container/container.test.ts b/test/container/container.test.ts index b997073..613ef3a 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -4,19 +4,21 @@ import { CircularDependencyError, ServiceNotFoundError } from '../../src/errors' import { ContainerRegistry } from '../../src/container/registry'; import { EMPTY_VALUE } from '../../src/types'; -afterEach(() => { - Container.of().reset('service'); - ContainerRegistry.removeContainer('container-reused'); - ContainerRegistry.removeContainer('container-first'); - ContainerRegistry.removeContainer('container-second'); - ContainerRegistry.removeContainer('container-scope'); - ContainerRegistry.removeContainer('singleton-forwarded'); - ContainerRegistry.removeContainer('container-reset-fallback'); - ContainerRegistry.removeContainer('container-local-has'); - ContainerRegistry.removeContainer('container-post-error'); -}); - describe('Container', () => { + afterEach(() => { + Container.of().reset('service'); + ContainerRegistry.removeContainer('container-reused'); + ContainerRegistry.removeContainer('container-first'); + ContainerRegistry.removeContainer('container-second'); + ContainerRegistry.removeContainer('container-scope'); + ContainerRegistry.removeContainer('container-binding'); + ContainerRegistry.removeContainer('container-binding-reset'); + ContainerRegistry.removeContainer('singleton-forwarded'); + ContainerRegistry.removeContainer('container-reset-fallback'); + ContainerRegistry.removeContainer('container-local-has'); + ContainerRegistry.removeContainer('container-post-error'); + }); + test('returns the default container for no id and the default id', () => { expect(Container.of()).toBe(Container.of('default')); }); @@ -72,6 +74,51 @@ describe('Container', () => { expect(requestContainer.get(SingletonService)).toBe(Container.of().get(SingletonService)); }); + test('returns a bound value set on the current container', () => { + class BoundService {} + + const bound = new BoundService(); + + Container.of().set(BoundService, bound); + + expect(Container.of().get(BoundService)).toBe(bound); + }); + + test('prefers a bound value over a registered service in the same container', () => { + class BoundService {} + + const bound = new BoundService(); + + Container.of().register({ + id: BoundService, + Class: BoundService, + name: 'BoundService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); + + Container.of().set(BoundService, bound); + + expect(Container.of().get(BoundService)).toBe(bound); + }); + + test('remove clears a bound value from the current container', () => { + const requestContainer = Container.of('container-binding'); + + class BoundService {} + + const bound = new BoundService(); + + requestContainer.set(BoundService, bound); + + expect(requestContainer.get(BoundService)).toBe(bound); + + requestContainer.remove(BoundService); + + expect(() => requestContainer.get(BoundService)).toThrow(ServiceNotFoundError); + }); + test('reset value clears cached instances but keeps registrations', () => { class ResettableService {} @@ -94,6 +141,17 @@ describe('Container', () => { expect(afterReset).not.toBe(beforeReset); }); + test('reset value keeps bound values intact', () => { + class BoundService {} + + const bound = new BoundService(); + + Container.of().set(BoundService, bound); + Container.of().reset('value'); + + expect(Container.of().get(BoundService)).toBe(bound); + }); + test('reset service removes registrations from the current container', () => { class ResettableService {} @@ -111,6 +169,19 @@ describe('Container', () => { expect(() => Container.of().get(ResettableService)).toThrow(ServiceNotFoundError); }); + test('reset service removes bound values from the current container', () => { + const requestContainer = Container.of('container-binding-reset'); + + class BoundService {} + + const bound = new BoundService(); + + requestContainer.set(BoundService, bound); + requestContainer.reset('service'); + + expect(() => requestContainer.get(BoundService)).toThrow(ServiceNotFoundError); + }); + test('reset service on a named container clears local copies and falls back to default registrations again', () => { const requestContainer = Container.of('container-reset-fallback'); From 66d9f9836b536b939420ffc5129ce32bc54c8d8a Mon Sep 17 00:00:00 2001 From: changchanghwang Date: Sun, 22 Mar 2026 18:44:18 +0900 Subject: [PATCH 4/4] test: refactor test structure --- test/container/container.test.ts | 505 ++++++++++++++++--------------- test/container/registry.test.ts | 42 +-- test/decorators/inject.test.ts | 141 +++++---- test/decorators/service.test.ts | 52 ++-- 4 files changed, 390 insertions(+), 350 deletions(-) diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 613ef3a..df78429 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -4,325 +4,346 @@ import { CircularDependencyError, ServiceNotFoundError } from '../../src/errors' import { ContainerRegistry } from '../../src/container/registry'; import { EMPTY_VALUE } from '../../src/types'; -describe('Container', () => { - afterEach(() => { - Container.of().reset('service'); - ContainerRegistry.removeContainer('container-reused'); - ContainerRegistry.removeContainer('container-first'); - ContainerRegistry.removeContainer('container-second'); - ContainerRegistry.removeContainer('container-scope'); - ContainerRegistry.removeContainer('container-binding'); - ContainerRegistry.removeContainer('container-binding-reset'); - ContainerRegistry.removeContainer('singleton-forwarded'); - ContainerRegistry.removeContainer('container-reset-fallback'); - ContainerRegistry.removeContainer('container-local-has'); - ContainerRegistry.removeContainer('container-post-error'); - }); +afterEach(() => { + Container.of().reset('service'); + ContainerRegistry.removeContainer('container'); + ContainerRegistry.removeContainer('container-reused'); + ContainerRegistry.removeContainer('container-first'); + ContainerRegistry.removeContainer('container-second'); + ContainerRegistry.removeContainer('container-scope'); + ContainerRegistry.removeContainer('container-binding'); + ContainerRegistry.removeContainer('container-binding-reset'); + ContainerRegistry.removeContainer('singleton-forwarded'); + ContainerRegistry.removeContainer('container-reset-fallback'); + ContainerRegistry.removeContainer('container-local-has'); + ContainerRegistry.removeContainer('container-post-error'); +}); - test('returns the default container for no id and the default id', () => { - expect(Container.of()).toBe(Container.of('default')); - }); +describe('Container', () => { + describe('of', () => { + test('returns the default container for no id and the default id', () => { + expect(Container.of()).toBe(Container.of('default')); + }); - test('reuses named containers for the same id', () => { - expect(Container.of('container-reused')).toBe(Container.of('container-reused')); - }); + test('reuses named containers for the same id', () => { + expect(Container.of('container-reused')).toBe(Container.of('container-reused')); + }); - test('creates distinct named containers for different ids', () => { - expect(Container.of('container-first')).not.toBe(Container.of('container-second')); + test('creates distinct named containers for different ids', () => { + expect(Container.of('container-first')).not.toBe(Container.of('container-second')); + }); }); - test('reuses container-scoped services within the same container and isolates them across containers', () => { - const requestContainer = Container.of('container-scope'); - - class RequestService {} - - Container.of().register({ - id: RequestService, - Class: RequestService, - name: 'RequestService', - injections: [], - scope: 'container', - value: EMPTY_VALUE, + describe('register', () => { + test('reuses container-scoped services within the same container and isolates them across containers', () => { + const requestContainer = Container.of('container-scope'); + + class RequestService {} + + Container.of().register({ + id: RequestService, + Class: RequestService, + name: 'RequestService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); + + const defaultFirst = Container.of().get(RequestService); + const defaultSecond = Container.of().get(RequestService); + const requestFirst = requestContainer.get(RequestService); + const requestSecond = requestContainer.get(RequestService); + + expect(defaultFirst).toBe(defaultSecond); + expect(requestFirst).toBe(requestSecond); + expect(defaultFirst).not.toBe(requestFirst); }); - const defaultFirst = Container.of().get(RequestService); - const defaultSecond = Container.of().get(RequestService); - const requestFirst = requestContainer.get(RequestService); - const requestSecond = requestContainer.get(RequestService); - - expect(defaultFirst).toBe(defaultSecond); - expect(requestFirst).toBe(requestSecond); - expect(defaultFirst).not.toBe(requestFirst); - }); + test('stores singleton registrations in the default container when set from a named container', () => { + const requestContainer = Container.of('singleton-forwarded'); - test('stores singleton registrations in the default container when set from a named container', () => { - const requestContainer = Container.of('singleton-forwarded'); + class SingletonService {} - class SingletonService {} + requestContainer.register({ + id: SingletonService, + Class: SingletonService, + name: 'SingletonService', + injections: [], + scope: 'singleton', + value: EMPTY_VALUE, + }); - requestContainer.register({ - id: SingletonService, - Class: SingletonService, - name: 'SingletonService', - injections: [], - scope: 'singleton', - value: EMPTY_VALUE, + expect(requestContainer.has(SingletonService)).toBe(false); + expect(Container.of().has(SingletonService)).toBe(true); + expect(requestContainer.get(SingletonService)).toBe(Container.of().get(SingletonService)); }); - - expect(requestContainer.has(SingletonService)).toBe(false); - expect(Container.of().has(SingletonService)).toBe(true); - expect(requestContainer.get(SingletonService)).toBe(Container.of().get(SingletonService)); }); - test('returns a bound value set on the current container', () => { - class BoundService {} + describe('set', () => { + test('returns a bound value set on the current container', () => { + class BoundService {} - const bound = new BoundService(); + const bound = new BoundService(); - Container.of().set(BoundService, bound); + Container.of().set(BoundService, bound); - expect(Container.of().get(BoundService)).toBe(bound); - }); + expect(Container.of().get(BoundService)).toBe(bound); + }); - test('prefers a bound value over a registered service in the same container', () => { - class BoundService {} + test('prefers a bound value over a registered service in the same container', () => { + class BoundService {} - const bound = new BoundService(); + const bound = new BoundService(); - Container.of().register({ - id: BoundService, - Class: BoundService, - name: 'BoundService', - injections: [], - scope: 'container', - value: EMPTY_VALUE, - }); + Container.of().register({ + id: BoundService, + Class: BoundService, + name: 'BoundService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); - Container.of().set(BoundService, bound); + Container.of().set(BoundService, bound); - expect(Container.of().get(BoundService)).toBe(bound); + expect(Container.of().get(BoundService)).toBe(bound); + }); }); - test('remove clears a bound value from the current container', () => { - const requestContainer = Container.of('container-binding'); - - class BoundService {} - - const bound = new BoundService(); + describe('remove', () => { + test('clears a bound value from the current container', () => { + const requestContainer = Container.of('container-binding'); - requestContainer.set(BoundService, bound); + class BoundService {} - expect(requestContainer.get(BoundService)).toBe(bound); + const bound = new BoundService(); - requestContainer.remove(BoundService); + requestContainer.set(BoundService, bound); - expect(() => requestContainer.get(BoundService)).toThrow(ServiceNotFoundError); - }); + expect(requestContainer.get(BoundService)).toBe(bound); - test('reset value clears cached instances but keeps registrations', () => { - class ResettableService {} + requestContainer.remove(BoundService); - Container.of().register({ - id: ResettableService, - Class: ResettableService, - name: 'ResettableService', - injections: [], - scope: 'container', - value: EMPTY_VALUE, + expect(() => requestContainer.get(BoundService)).toThrow(ServiceNotFoundError); }); + }); - const beforeReset = Container.of().get(ResettableService); + describe('reset', () => { + describe("'value'", () => { + test('clears cached instances but keeps registrations', () => { + class ResettableService {} - Container.of().reset('value'); + Container.of().register({ + id: ResettableService, + Class: ResettableService, + name: 'ResettableService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); - const afterReset = Container.of().get(ResettableService); + const beforeReset = Container.of().get(ResettableService); - expect(afterReset).toBeInstanceOf(ResettableService); - expect(afterReset).not.toBe(beforeReset); - }); + Container.of().reset('value'); - test('reset value keeps bound values intact', () => { - class BoundService {} + const afterReset = Container.of().get(ResettableService); - const bound = new BoundService(); + expect(afterReset).toBeInstanceOf(ResettableService); + expect(afterReset).not.toBe(beforeReset); + }); - Container.of().set(BoundService, bound); - Container.of().reset('value'); + test('keeps bound values intact', () => { + class BoundService {} - expect(Container.of().get(BoundService)).toBe(bound); - }); + const bound = new BoundService(); - test('reset service removes registrations from the current container', () => { - class ResettableService {} + Container.of().set(BoundService, bound); + Container.of().reset('value'); - Container.of().register({ - id: ResettableService, - Class: ResettableService, - name: 'ResettableService', - injections: [], - scope: 'container', - value: EMPTY_VALUE, + expect(Container.of().get(BoundService)).toBe(bound); + }); }); - Container.of().reset('service'); + describe("'service'", () => { + test('removes registrations from the current container', () => { + class ResettableService {} - expect(() => Container.of().get(ResettableService)).toThrow(ServiceNotFoundError); - }); + Container.of().register({ + id: ResettableService, + Class: ResettableService, + name: 'ResettableService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); - test('reset service removes bound values from the current container', () => { - const requestContainer = Container.of('container-binding-reset'); + Container.of().reset('service'); - class BoundService {} + expect(() => Container.of().get(ResettableService)).toThrow(ServiceNotFoundError); + }); - const bound = new BoundService(); + test('removes bound values from the current container', () => { + const requestContainer = Container.of('container-binding-reset'); - requestContainer.set(BoundService, bound); - requestContainer.reset('service'); + class BoundService {} - expect(() => requestContainer.get(BoundService)).toThrow(ServiceNotFoundError); - }); + const bound = new BoundService(); - test('reset service on a named container clears local copies and falls back to default registrations again', () => { - const requestContainer = Container.of('container-reset-fallback'); + requestContainer.set(BoundService, bound); + requestContainer.reset('service'); - class ResettableService {} + expect(() => requestContainer.get(BoundService)).toThrow(ServiceNotFoundError); + }); - Container.of().register({ - id: ResettableService, - Class: ResettableService, - name: 'ResettableService', - injections: [], - scope: 'container', - value: EMPTY_VALUE, - }); + test('on a named container clears local copies and falls back to default registrations again', () => { + const requestContainer = Container.of('container-reset-fallback'); - const first = requestContainer.get(ResettableService); + class ResettableService {} - requestContainer.reset('service'); + Container.of().register({ + id: ResettableService, + Class: ResettableService, + name: 'ResettableService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); - const second = requestContainer.get(ResettableService); + const first = requestContainer.get(ResettableService); - expect(second).toBeInstanceOf(ResettableService); - expect(second).not.toBe(first); - expect(second).not.toBe(Container.of().get(ResettableService)); - }); + requestContainer.reset('service'); - test('reset specific container with static method', () => { - class ResettableService {} + const second = requestContainer.get(ResettableService); - Container.of('container').register({ - id: ResettableService, - Class: ResettableService, - name: 'ResettableService', - injections: [], - scope: 'container', - value: EMPTY_VALUE, + expect(second).toBeInstanceOf(ResettableService); + expect(second).not.toBe(first); + expect(second).not.toBe(Container.of().get(ResettableService)); + }); }); - Container.reset('container', { strategy: 'service' }); - - expect(() => ContainerRegistry.getContainer('container')?.get(ResettableService)).toThrow(ServiceNotFoundError); - }); + describe('static reset', () => { + test('resets a specific container', () => { + class ResettableService {} - test('has only reports local registrations', () => { - const requestContainer = Container.of('container-local-has'); + Container.of('container').register({ + id: ResettableService, + Class: ResettableService, + name: 'ResettableService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); - class ScopedService {} + Container.reset('container', { strategy: 'service' }); - Container.of().register({ - id: ScopedService, - Class: ScopedService, - name: 'ScopedService', - injections: [], - scope: 'container', - value: EMPTY_VALUE, + expect(() => ContainerRegistry.getContainer('container')?.get(ResettableService)).toThrow(ServiceNotFoundError); + }); }); + }); - expect(Container.of().has(ScopedService)).toBe(true); - expect(requestContainer.has(ScopedService)).toBe(false); + describe('has', () => { + test('only reports local registrations', () => { + const requestContainer = Container.of('container-local-has'); - requestContainer.get(ScopedService); + class ScopedService {} - expect(requestContainer.has(ScopedService)).toBe(true); - }); + Container.of().register({ + id: ScopedService, + Class: ScopedService, + name: 'ScopedService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); - test('throws when a service is missing', () => { - class MissingService {} + expect(Container.of().has(ScopedService)).toBe(true); + expect(requestContainer.has(ScopedService)).toBe(false); - expect(() => Container.of().get(MissingService)).toThrow(ServiceNotFoundError); - }); - - test('throws when dependencies form a circular graph', () => { - class AlphaService { - public beta!: BetaService; - } - - class BetaService { - public alpha!: AlphaService; - } - - Container.of().register({ - id: AlphaService, - Class: AlphaService, - name: 'AlphaService', - injections: [{ id: BetaService, name: 'beta' }], - scope: 'container', - value: EMPTY_VALUE, - }); + requestContainer.get(ScopedService); - Container.of().register({ - id: BetaService, - Class: BetaService, - name: 'BetaService', - injections: [{ id: AlphaService, name: 'alpha' }], - scope: 'container', - value: EMPTY_VALUE, + expect(requestContainer.has(ScopedService)).toBe(true); }); - - expect(() => Container.of().get(AlphaService)).toThrow(CircularDependencyError); }); - test('recovers cleanly after a circular resolution error', () => { - const requestContainer = Container.of('container-post-error'); - - class AlphaService { - public beta!: BetaService; - } - - class BetaService { - public alpha!: AlphaService; - } + describe('get', () => { + test('throws when a service is missing', () => { + class MissingService {} - class HealthyService {} - - requestContainer.register({ - id: AlphaService, - Class: AlphaService, - name: 'AlphaService', - injections: [{ id: BetaService, name: 'beta' }], - scope: 'container', - value: EMPTY_VALUE, + expect(() => Container.of().get(MissingService)).toThrow(ServiceNotFoundError); }); - requestContainer.register({ - id: BetaService, - Class: BetaService, - name: 'BetaService', - injections: [{ id: AlphaService, name: 'alpha' }], - scope: 'container', - value: EMPTY_VALUE, + test('throws when dependencies form a circular graph', () => { + class AlphaService { + public beta!: BetaService; + } + + class BetaService { + public alpha!: AlphaService; + } + + Container.of().register({ + id: AlphaService, + Class: AlphaService, + name: 'AlphaService', + injections: [{ id: BetaService, name: 'beta' }], + scope: 'container', + value: EMPTY_VALUE, + }); + + Container.of().register({ + id: BetaService, + Class: BetaService, + name: 'BetaService', + injections: [{ id: AlphaService, name: 'alpha' }], + scope: 'container', + value: EMPTY_VALUE, + }); + + expect(() => Container.of().get(AlphaService)).toThrow(CircularDependencyError); }); - requestContainer.register({ - id: HealthyService, - Class: HealthyService, - name: 'HealthyService', - injections: [], - scope: 'container', - value: EMPTY_VALUE, + test('recovers cleanly after a circular resolution error', () => { + const requestContainer = Container.of('container-post-error'); + + class AlphaService { + public beta!: BetaService; + } + + class BetaService { + public alpha!: AlphaService; + } + + class HealthyService {} + + requestContainer.register({ + id: AlphaService, + Class: AlphaService, + name: 'AlphaService', + injections: [{ id: BetaService, name: 'beta' }], + scope: 'container', + value: EMPTY_VALUE, + }); + + requestContainer.register({ + id: BetaService, + Class: BetaService, + name: 'BetaService', + injections: [{ id: AlphaService, name: 'alpha' }], + scope: 'container', + value: EMPTY_VALUE, + }); + + requestContainer.register({ + id: HealthyService, + Class: HealthyService, + name: 'HealthyService', + injections: [], + scope: 'container', + value: EMPTY_VALUE, + }); + + expect(() => requestContainer.get(AlphaService)).toThrow(CircularDependencyError); + expect(requestContainer.get(HealthyService)).toBeInstanceOf(HealthyService); }); - - expect(() => requestContainer.get(AlphaService)).toThrow(CircularDependencyError); - expect(requestContainer.get(HealthyService)).toBeInstanceOf(HealthyService); }); }); diff --git a/test/container/registry.test.ts b/test/container/registry.test.ts index 86d8d95..0c56ecd 100644 --- a/test/container/registry.test.ts +++ b/test/container/registry.test.ts @@ -11,31 +11,37 @@ afterEach(() => { }); describe('ContainerRegistry', () => { - test('throws when registering another container with the same id', () => { - Container.of('registry-duplicate'); - - expect(() => ContainerRegistry.registerContainer(new Container('registry-duplicate'))).toThrow( - ContainerDuplicatedError, - ); + describe('registerContainer', () => { + test('throws when registering another container with the same id', () => { + Container.of('registry-duplicate'); + + expect(() => ContainerRegistry.registerContainer(new Container('registry-duplicate'))).toThrow( + ContainerDuplicatedError, + ); + }); }); - test('reports named container presence through hasContainer and getContainer', () => { - const container = Container.of('registry-visible'); + describe('hasContainer and getContainer', () => { + test('report named container presence', () => { + const container = Container.of('registry-visible'); - expect(ContainerRegistry.hasContainer('registry-visible')).toBe(true); - expect(ContainerRegistry.getContainer('registry-visible')).toBe(container); + expect(ContainerRegistry.hasContainer('registry-visible')).toBe(true); + expect(ContainerRegistry.getContainer('registry-visible')).toBe(container); + }); }); - test('removes a named container from the registry', () => { - Container.of('registry-removed'); + describe('removeContainer', () => { + test('removes a named container from the registry', () => { + Container.of('registry-removed'); - ContainerRegistry.removeContainer('registry-removed'); + ContainerRegistry.removeContainer('registry-removed'); - expect(ContainerRegistry.hasContainer('registry-removed')).toBe(false); - expect(ContainerRegistry.getContainer('registry-removed')).toBeUndefined(); - }); + expect(ContainerRegistry.hasContainer('registry-removed')).toBe(false); + expect(ContainerRegistry.getContainer('registry-removed')).toBeUndefined(); + }); - test('throws when removing the default container', () => { - expect(() => ContainerRegistry.removeContainer('default')).toThrow(DefaultContainerIdError); + test('throws when removing the default container', () => { + expect(() => ContainerRegistry.removeContainer('default')).toThrow(DefaultContainerIdError); + }); }); }); diff --git a/test/decorators/inject.test.ts b/test/decorators/inject.test.ts index 8170327..2bf32b5 100644 --- a/test/decorators/inject.test.ts +++ b/test/decorators/inject.test.ts @@ -7,85 +7,92 @@ afterEach(() => { ContainerRegistry.removeContainer('inject-named-container'); }); -describe('Inject Decorator', () => { - test('injects a decorated dependency into a decorated class field', () => { - @Service() - class LoggerService {} - - @Service() - class HandlerService { - @Inject(LoggerService) - public logger!: LoggerService; - } - - const handler = Container.of().get(HandlerService); - const descriptor = Object.getOwnPropertyDescriptor(handler, 'logger'); - - expect(handler.logger).toBeInstanceOf(LoggerService); - expect(handler.logger).toBe(Container.of().get(LoggerService)); - expect(descriptor).toMatchObject({ - configurable: true, - writable: true, - value: handler.logger, +describe('Inject decorator', () => { + describe('@Inject()', () => { + test('injects a decorated dependency into a decorated class field', () => { + @Service() + class LoggerService {} + + @Service() + class HandlerService { + @Inject(LoggerService) + public logger!: LoggerService; + } + + const handler = Container.of().get(HandlerService); + const descriptor = Object.getOwnPropertyDescriptor(handler, 'logger'); + + expect(handler.logger).toBeInstanceOf(LoggerService); + expect(handler.logger).toBe(Container.of().get(LoggerService)); + expect(descriptor).toMatchObject({ + configurable: true, + writable: true, + value: handler.logger, + }); }); - }); - test('injects multiple decorated dependencies on the same class', () => { - @Service() - class LoggerService {} + test('injects multiple decorated dependencies on the same class', () => { + @Service() + class LoggerService {} - @Service() - class MetricsService {} + @Service() + class MetricsService {} - @Service() - class HandlerService { - @Inject(LoggerService) - public logger!: LoggerService; + @Service() + class HandlerService { + @Inject(LoggerService) + public logger!: LoggerService; - @Inject(MetricsService) - public metrics!: MetricsService; - } + @Inject(MetricsService) + public metrics!: MetricsService; + } - const handler = Container.of().get(HandlerService); + const handler = Container.of().get(HandlerService); - expect(handler.logger).toBe(Container.of().get(LoggerService)); - expect(handler.metrics).toBe(Container.of().get(MetricsService)); - }); + expect(handler.logger).toBe(Container.of().get(LoggerService)); + expect(handler.metrics).toBe(Container.of().get(MetricsService)); + }); - test('uses the current named container when resolving injected dependencies', () => { - const requestContainer = Container.of('inject-named-container'); + test('uses the current named container when resolving injected dependencies', () => { + const requestContainer = Container.of('inject-named-container'); - @Service() - class LoggerService {} + @Service() + class LoggerService {} - @Service() - class HandlerService { - @Inject(LoggerService) - public logger!: LoggerService; - } + @Service() + class HandlerService { + @Inject(LoggerService) + public logger!: LoggerService; + } - const handler = requestContainer.get(HandlerService); + const handler = requestContainer.get(HandlerService); - expect(handler.logger).toBe(requestContainer.get(LoggerService)); - expect(handler.logger).not.toBe(Container.of().get(LoggerService)); - }); + expect(handler.logger).toBe(requestContainer.get(LoggerService)); + expect(handler.logger).not.toBe(Container.of().get(LoggerService)); + }); + + test('injects a token-identified dependency into a decorated class field', () => { + interface Logger { + log(message: string): void; + } - test('injects a token-identified dependency into a decorated class field', () => { - interface Logger { - log(message: string): void; - } - const LOGGER = new Token('Logger'); - @Service(LOGGER) - class ConsoleLogger implements Logger { - public log(_: string) {} - } - @Service() - class HandlerService { - @Inject(LOGGER) - public logger!: Logger; - } - const handler = Container.of().get(HandlerService); - expect(handler.logger).toBeInstanceOf(ConsoleLogger); - expect(handler.logger).toBe(Container.of().get(LOGGER)); + const LOGGER = new Token('Logger'); + + @Service(LOGGER) + class ConsoleLogger implements Logger { + public log(_: string) {} + } + + @Service() + class HandlerService { + @Inject(LOGGER) + public logger!: Logger; + } + + const handler = Container.of().get(HandlerService); + + expect(handler.logger).toBeInstanceOf(ConsoleLogger); + expect(handler.logger).toBe(Container.of().get(LOGGER)); + }); }); }); diff --git a/test/decorators/service.test.ts b/test/decorators/service.test.ts index 6735510..ba04712 100644 --- a/test/decorators/service.test.ts +++ b/test/decorators/service.test.ts @@ -8,38 +8,44 @@ afterEach(() => { ContainerRegistry.removeContainer('service-singleton-container'); }); -describe('Service Decorator', () => { - test('registers a decorated class in the default container', () => { - @Service() - class TestService {} - - expect(Container.of().get(TestService)).toBeInstanceOf(TestService); +describe('Service decorator', () => { + describe('@Service()', () => { + test('registers a decorated class in the default container', () => { + @Service() + class TestService {} + + expect(Container.of().get(TestService)).toBeInstanceOf(TestService); + }); }); - test('supports custom service identifiers', () => { - @Service({ id: 'custom-service' }) - class NamedService {} + describe('@Service({ id })', () => { + test('supports custom service identifiers', () => { + @Service({ id: 'custom-service' }) + class NamedService {} - expect(Container.of().get('custom-service')).toBeInstanceOf(NamedService); - expect(() => Container.of().get(NamedService)).toThrow(ServiceNotFoundError); + expect(Container.of().get('custom-service')).toBeInstanceOf(NamedService); + expect(() => Container.of().get(NamedService)).toThrow(ServiceNotFoundError); + }); }); - test('honors transient scope declared through the decorator', () => { - @Service({ scope: 'transient' }) - class TransientService {} + describe('@Service({ scope })', () => { + test('honors transient scope declared through the decorator', () => { + @Service({ scope: 'transient' }) + class TransientService {} - const first = Container.of().get(TransientService); - const second = Container.of().get(TransientService); + const first = Container.of().get(TransientService); + const second = Container.of().get(TransientService); - expect(first).not.toBe(second); - }); + expect(first).not.toBe(second); + }); - test('honors singleton scope declared through the decorator across containers', () => { - const requestContainer = Container.of('service-singleton-container'); + test('honors singleton scope declared through the decorator across containers', () => { + const requestContainer = Container.of('service-singleton-container'); - @Service({ scope: 'singleton' }) - class SingletonService {} + @Service({ scope: 'singleton' }) + class SingletonService {} - expect(Container.of().get(SingletonService)).toBe(requestContainer.get(SingletonService)); + expect(Container.of().get(SingletonService)).toBe(requestContainer.get(SingletonService)); + }); }); });