diff --git a/specs/container.spec.ts b/specs/container.spec.ts index 8c1fe9b..a71287a 100644 --- a/specs/container.spec.ts +++ b/specs/container.spec.ts @@ -1,20 +1,19 @@ -import {MyService} from "./MyService"; -import {MyServiceInterface} from "./MyServiceInterface"; -import {SayHelloType} from "./SayHelloType"; -import {sayHelloWorld} from "./sayHelloWorld"; -import {MyUseCase} from "./MyUseCase"; -import {MyUseCaseInterface} from "./MyUseCaseInterface"; -import {LoggerInterface} from "./LoggerInterface"; -import {DI} from "./DI"; -import {Container, createContainer, createModule} from "../src"; -import {MyServiceClass} from "./MyServiceClass"; -import {MyServiceClassInterface} from "./MyServiceClassInterface"; -import {FunctionWithDependencies} from "./FunctionWithDependencies"; -import {HigherOrderFunctionWithoutDependencies} from "./HigherOrderFunctionWithoutDependencies"; -import {ServiceWithoutDependencyInterface} from "./ServiceWithoutDependencyInterface"; -import {MyServiceClassWithoutDependencies} from "./MyServiceClassWithoutDependencies"; +import {MyService} from "./examples/MyService"; +import {MyServiceInterface} from "./examples/MyServiceInterface"; +import {SayHelloType} from "./examples/SayHelloType"; +import {sayHelloWorld} from "./examples/sayHelloWorld"; +import {MyUseCase} from "./examples/MyUseCase"; +import {MyUseCaseInterface} from "./examples/MyUseCaseInterface"; +import {LoggerInterface} from "./examples/LoggerInterface"; +import {DI} from "./examples/DI"; +import {Container, createContainer} from "../src"; +import {MyServiceClass} from "./examples/MyServiceClass"; +import {MyServiceClassInterface} from "./examples/MyServiceClassInterface"; +import {FunctionWithDependencies} from "./examples/FunctionWithDependencies"; +import {HigherOrderFunctionWithoutDependencies} from "./examples/HigherOrderFunctionWithoutDependencies"; +import {ServiceWithoutDependencyInterface} from "./examples/ServiceWithoutDependencyInterface"; +import {MyServiceClassWithoutDependencies} from "./examples/MyServiceClassWithoutDependencies"; import {mock, MockProxy} from "vitest-mock-extended"; -import {vi} from "vitest"; describe('Container', () => { @@ -78,77 +77,6 @@ describe('Container', () => { .toThrowError('Invalid dependencies type'); }); }); - - describe('When the scope is defined to "transient"', () => { - it('should return a new instance each time', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.MY_SERVICE) - .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'transient'); - - // Act - const myService1 = container.get(DI.MY_SERVICE); - const myService2 = container.get(DI.MY_SERVICE); - - // Assert - expect(myService1).not.toBe(myService2); - }); - }); - - describe('When the scope is defined to "scoped"', () => { - it('should return the same instance within the same scope', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.MY_SERVICE) - .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'scoped'); - - // Act & Assert - container.runInScope(() => { - const myService1 = container.get(DI.MY_SERVICE); - const myService2 = container.get(DI.MY_SERVICE); - expect(myService1).toBe(myService2); - }); - }); - - it('should return different instances in different scopes', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.MY_SERVICE) - .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'scoped'); - - // Act - let myService1: MyServiceInterface | undefined; - let myService2: MyServiceInterface | undefined; - - container.runInScope(() => { - myService1 = container.get(DI.MY_SERVICE); - }); - - container.runInScope(() => { - myService2 = container.get(DI.MY_SERVICE); - }); - - // Assert - expect(myService1).toBeDefined(); - expect(myService2).toBeDefined(); - expect(myService1).not.toBe(myService2); - }); - }); - - describe('When a scoped dependency is resolved outside of a scope', () => { - it('should throw an error', () => { - // Arrange - container.bind(DI.MY_SERVICE) - .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'scoped'); - - // Act & Assert - expect(() => container.get(DI.MY_SERVICE)) - .toThrowError(`Cannot resolve scoped binding outside of a scope: ${DI.MY_SERVICE.toString()}`); - }); - }); }); describe.each([ @@ -226,117 +154,6 @@ describe('Container', () => { expect(fakeLogger.log).toHaveBeenCalledWith('hello world'); }); }); - - describe('When the instance is retrieved twice', () => { - describe('When the scope is not defined', () => { - it('should return the same instance (singleton)', () => { - // Arrange - const factoryCalls = vi.fn(); - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - - container.bind(DI.MY_SERVICE).toFactory(() => { - factoryCalls(); - return MyService({ - dep1: container.get(DI.DEP1), - dep2: container.get(DI.DEP2) - }); - }); - const myService1 = container.get(DI.MY_SERVICE); - - // Act - const myService2 = container.get(DI.MY_SERVICE); - - // Assert - expect(myService1).toBe(myService2); - expect(factoryCalls).toHaveBeenCalledTimes(1); - }); - }); - - describe('When the scope is defined to "transient"', () => { - it('should return a new instance each time', () => { - // Arrange - const factoryCalls = vi.fn(); - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - - container.bind(DI.MY_SERVICE).toFactory(() => { - factoryCalls(); - return MyService({ - dep1: container.get(DI.DEP1), - dep2: container.get(DI.DEP2) - }); - }, 'transient'); - - // Act - const myService1 = container.get(DI.MY_SERVICE); - const myService2 = container.get(DI.MY_SERVICE); - - // Assert - expect(myService1).not.toBe(myService2); - expect(factoryCalls).toHaveBeenCalledTimes(2); - }); - }); - - describe('When the scope is defined to "scoped"', () => { - it('should return the same instance within the same scope', () => { - // Arrange - const factoryCalls = vi.fn(); - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - - container.bind(DI.MY_SERVICE).toFactory(() => { - factoryCalls(); - return MyService({ - dep1: container.get(DI.DEP1), - dep2: container.get(DI.DEP2) - }); - }, 'scoped'); - - // Act & Assert - container.runInScope(() => { - const myService1 = container.get(DI.MY_SERVICE); - const myService2 = container.get(DI.MY_SERVICE); - - expect(myService1).toBe(myService2); - expect(factoryCalls).toHaveBeenCalledTimes(1); - }); - }); - - it('should return different instances in different scopes', () => { - // Arrange - const factoryCalls = vi.fn(); - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - - container.bind(DI.MY_SERVICE).toFactory(() => { - factoryCalls(); - return MyService({ - dep1: container.get(DI.DEP1), - dep2: container.get(DI.DEP2) - }); - }, 'scoped'); - - // Act - let myService1: MyServiceInterface | undefined; - let myService2: MyServiceInterface | undefined; - - container.runInScope(() => { - myService1 = container.get(DI.MY_SERVICE); - }); - - container.runInScope(() => { - myService2 = container.get(DI.MY_SERVICE); - }); - - // Assert - expect(myService1).toBeDefined(); - expect(myService2).toBeDefined(); - expect(myService1).not.toBe(myService2); - expect(factoryCalls).toHaveBeenCalledTimes(2); - }); - }); - }); }); }); @@ -368,109 +185,6 @@ describe('Container', () => { expect(myService.runTask()).toBe('Executing without dependencies'); }); }); - - describe('When the instance is retrieved twice', () => { - describe('When the scope is not defined', () => { - it('should return the same instance (singleton)', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]); - const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); - - // Act - const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); - - // Assert - expect(myService1).toBe(myService2); - }); - }); - - describe('When the scope is defined to "singleton"', () => { - it('should return the same instance', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'singleton'); - const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); - - // Act - const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); - - // Assert - expect(myService1).toBe(myService2); - }); - }); - - describe('When the scope is defined to "transient"', () => { - it('should return a new instance each time', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'transient'); - - // Act - const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); - const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); - - // Assert - expect(myService1).not.toBe(myService2); - }); - }); - - describe('When the scope is defined to "scoped"', () => { - it('should return the same instance within the same scope', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'scoped'); - - // Act - container.runInScope(() => { - const myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); - const myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); - - // Assert - expect(myService1).toBe(myService2); - }); - }); - - it('should return different instances in different scopes', () => { - // Arrange - container.bind(DI.DEP1).toValue('dependency1'); - container.bind(DI.DEP2).toValue(42); - container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'scoped'); - - // Act - let myService1: MyServiceClassInterface | undefined; - let myService2: MyServiceClassInterface | undefined; - - container.runInScope(() => { - myService1 = container.get(DI.CLASS_WITH_DEPENDENCIES); - }); - - container.runInScope(() => { - myService2 = container.get(DI.CLASS_WITH_DEPENDENCIES); - }); - - // Assert - expect(myService1).toBeDefined(); - expect(myService2).toBeDefined(); - expect(myService1).not.toBe(myService2); - }); - }); - - describe('When a scoped dependency is resolved outside of a scope', () => { - it('should throw an error', () => { - // Arrange - container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2], 'scoped'); - - // Act & Assert - expect(() => container.get(DI.CLASS_WITH_DEPENDENCIES)) - .toThrowError(`Cannot resolve scoped binding outside of a scope: ${DI.CLASS_WITH_DEPENDENCIES.toString()}`); - }); - }); - }); }); describe('When no dependency has been registered', () => { @@ -480,129 +194,4 @@ describe('Container', () => { .toThrowError(`No binding found for key: ${DI.NOT_REGISTERED_VALUE.toString()}`); }); }); - - describe('When a module is loaded', () => { - it('should return all module dependencies', () => { - // Arrange - const myModule = createModule(); - myModule.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld); - container.load(Symbol('myModule'), myModule); - - // Act - const sayHello = container.get(DI.SIMPLE_FUNCTION); - - // Assert - expect(sayHello()).toBe('hello world'); - }); - - describe('When a dependency of the module is registered in another module', () => { - it('should correctly resolve all dependencies', () => { - // Arrange - const module1 = createModule(); - module1.bind(DI.DEP1).toValue('dependency1'); - - const module2 = createModule(); - module2.bind(DI.DEP2).toValue(42); - - const module3 = createModule(); - module3.bind(DI.MY_SERVICE).toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}); - - container.load(Symbol('module1'), module1); - container.load(Symbol('module2'), module2); - container.load(Symbol('module3'), module3); - - // Act - const myService = container.get(DI.MY_SERVICE); - - // Assert - expect(myService.runTask()).toBe('Executing with dep1: dependency1 and dep2: 42'); - }); - - it('should take the last registered values', () => { - // Arrange - const module1 = createModule(); - module1.bind(DI.DEP1).toValue('OLD dependency1'); - module1.bind(DI.MY_SERVICE).toFunction(sayHelloWorld); - - const module2 = createModule(); - module2.bind(DI.DEP1).toValue('NEW dependency1'); - - const module3 = createModule(); - module3.bind(DI.MY_SERVICE).toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}); - - container.bind(DI.DEP2).toValue(42); - container.load(Symbol('module1'), module1); - container.load(Symbol('module2'), module2); - container.load(Symbol('module3'), module3); - - // Act - const myService = container.get(DI.MY_SERVICE); - - // Assert - expect(myService.runTask()).toBe('Executing with dep1: NEW dependency1 and dep2: 42'); - }); - }); - }); - - describe('When a module is unloaded', () => { - describe('When another module has this dependency already registered', () => { - it('should use the existing dependency', () => { - // Arrange - const MODULE1 = Symbol('myModule1'); - const MODULE2 = Symbol('myModule2'); - - const module1 = createModule(); - module1.bind(DI.SIMPLE_FUNCTION).toFunction(() => { - return 'module 1 hello world'; - }); - container.load(MODULE1, module1); - - const module2 = createModule(); - module2.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld); - container.load(MODULE2, module2); - - const sayHelloBeforeUnload = container.get(DI.SIMPLE_FUNCTION); - expect(sayHelloBeforeUnload()).toBe('hello world'); - - // Act - container.unload(MODULE2); - - // Assert - const sayHelloAfterUnload = container.get(DI.SIMPLE_FUNCTION); - expect(sayHelloAfterUnload()).toBe('module 1 hello world'); - }); - }); - - describe('When no other module has this dependency already registered', () => { - it('should remove all its dependencies', () => { - // Arrange - const MY_MODULE = Symbol('myModule'); - - const module = createModule(); - module.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld); - container.load(MY_MODULE, module); - - const sayHelloBeforeUnload = container.get(DI.SIMPLE_FUNCTION); - expect(sayHelloBeforeUnload()).toBe('hello world'); - - // Act - container.unload(MY_MODULE); - - // Assert - expect(() => container.get(DI.SIMPLE_FUNCTION)) - .toThrowError(`No binding found for key: ${DI.SIMPLE_FUNCTION.toString()}`); - }); - }); - }); - - describe('When an unknown scope is used during binding', () => { - it('should throw an error', () => { - // Arrange - container.bind(DI.MY_SERVICE).toFunction(sayHelloWorld, 'unknown' as any); - - // Act & Assert - expect(() => container.get(DI.MY_SERVICE)) - .toThrowError('Unknown scope: unknown'); - }); - }); }); diff --git a/specs/DI.ts b/specs/examples/DI.ts similarity index 93% rename from specs/DI.ts rename to specs/examples/DI.ts index 082dff3..51d8b9f 100644 --- a/specs/DI.ts +++ b/specs/examples/DI.ts @@ -1,4 +1,4 @@ -import {InjectionTokens} from "../src"; +import {InjectionTokens} from "../../src"; export const DI: InjectionTokens = { DEP1: Symbol('DEP1'), diff --git a/specs/FunctionWithDependencies.ts b/specs/examples/FunctionWithDependencies.ts similarity index 100% rename from specs/FunctionWithDependencies.ts rename to specs/examples/FunctionWithDependencies.ts diff --git a/specs/HigherOrderFunctionWithoutDependencies.ts b/specs/examples/HigherOrderFunctionWithoutDependencies.ts similarity index 100% rename from specs/HigherOrderFunctionWithoutDependencies.ts rename to specs/examples/HigherOrderFunctionWithoutDependencies.ts diff --git a/specs/LoggerInterface.ts b/specs/examples/LoggerInterface.ts similarity index 100% rename from specs/LoggerInterface.ts rename to specs/examples/LoggerInterface.ts diff --git a/specs/MyService.ts b/specs/examples/MyService.ts similarity index 100% rename from specs/MyService.ts rename to specs/examples/MyService.ts diff --git a/specs/MyServiceClass.ts b/specs/examples/MyServiceClass.ts similarity index 100% rename from specs/MyServiceClass.ts rename to specs/examples/MyServiceClass.ts diff --git a/specs/MyServiceClassInterface.ts b/specs/examples/MyServiceClassInterface.ts similarity index 100% rename from specs/MyServiceClassInterface.ts rename to specs/examples/MyServiceClassInterface.ts diff --git a/specs/MyServiceClassWithoutDependencies.ts b/specs/examples/MyServiceClassWithoutDependencies.ts similarity index 100% rename from specs/MyServiceClassWithoutDependencies.ts rename to specs/examples/MyServiceClassWithoutDependencies.ts diff --git a/specs/MyServiceInterface.ts b/specs/examples/MyServiceInterface.ts similarity index 100% rename from specs/MyServiceInterface.ts rename to specs/examples/MyServiceInterface.ts diff --git a/specs/MyUseCase.ts b/specs/examples/MyUseCase.ts similarity index 100% rename from specs/MyUseCase.ts rename to specs/examples/MyUseCase.ts diff --git a/specs/MyUseCaseInterface.ts b/specs/examples/MyUseCaseInterface.ts similarity index 100% rename from specs/MyUseCaseInterface.ts rename to specs/examples/MyUseCaseInterface.ts diff --git a/specs/SayHelloType.ts b/specs/examples/SayHelloType.ts similarity index 100% rename from specs/SayHelloType.ts rename to specs/examples/SayHelloType.ts diff --git a/specs/ServiceWithoutDependencyInterface.ts b/specs/examples/ServiceWithoutDependencyInterface.ts similarity index 100% rename from specs/ServiceWithoutDependencyInterface.ts rename to specs/examples/ServiceWithoutDependencyInterface.ts diff --git a/specs/sayHelloWorld.ts b/specs/examples/sayHelloWorld.ts similarity index 100% rename from specs/sayHelloWorld.ts rename to specs/examples/sayHelloWorld.ts diff --git a/specs/module.spec.ts b/specs/module.spec.ts new file mode 100644 index 0000000..8e1b4a7 --- /dev/null +++ b/specs/module.spec.ts @@ -0,0 +1,129 @@ +import {Container, createContainer, createModule} from "../src"; +import {DI} from "./examples/DI"; +import {sayHelloWorld} from "./examples/sayHelloWorld"; +import {SayHelloType} from "./examples/SayHelloType"; +import {MyService} from "./examples/MyService"; +import {MyServiceInterface} from "./examples/MyServiceInterface"; + +describe('Module', () => { + + let container: Container; + + beforeEach(() => { + container = createContainer(); + }); + + describe('When a module is loaded', () => { + it('should return all module dependencies', () => { + // Arrange + const myModule = createModule(); + myModule.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld); + container.load(Symbol('myModule'), myModule); + + // Act + const sayHello = container.get(DI.SIMPLE_FUNCTION); + + // Assert + expect(sayHello()).toBe('hello world'); + }); + + describe('When a dependency of the module is registered in another module', () => { + it('should correctly resolve all dependencies', () => { + // Arrange + const module1 = createModule(); + module1.bind(DI.DEP1).toValue('dependency1'); + + const module2 = createModule(); + module2.bind(DI.DEP2).toValue(42); + + const module3 = createModule(); + module3.bind(DI.MY_SERVICE).toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}); + + container.load(Symbol('module1'), module1); + container.load(Symbol('module2'), module2); + container.load(Symbol('module3'), module3); + + // Act + const myService = container.get(DI.MY_SERVICE); + + // Assert + expect(myService.runTask()).toBe('Executing with dep1: dependency1 and dep2: 42'); + }); + + it('should take the last registered values', () => { + // Arrange + const module1 = createModule(); + module1.bind(DI.DEP1).toValue('OLD dependency1'); + module1.bind(DI.MY_SERVICE).toFunction(sayHelloWorld); + + const module2 = createModule(); + module2.bind(DI.DEP1).toValue('NEW dependency1'); + + const module3 = createModule(); + module3.bind(DI.MY_SERVICE).toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}); + + container.bind(DI.DEP2).toValue(42); + container.load(Symbol('module1'), module1); + container.load(Symbol('module2'), module2); + container.load(Symbol('module3'), module3); + + // Act + const myService = container.get(DI.MY_SERVICE); + + // Assert + expect(myService.runTask()).toBe('Executing with dep1: NEW dependency1 and dep2: 42'); + }); + }); + }); + + describe('When a module is unloaded', () => { + describe('When another module has this dependency already registered', () => { + it('should use the existing dependency', () => { + // Arrange + const MODULE1 = Symbol('myModule1'); + const MODULE2 = Symbol('myModule2'); + + const module1 = createModule(); + module1.bind(DI.SIMPLE_FUNCTION).toFunction(() => { + return 'module 1 hello world'; + }); + container.load(MODULE1, module1); + + const module2 = createModule(); + module2.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld); + container.load(MODULE2, module2); + + const sayHelloBeforeUnload = container.get(DI.SIMPLE_FUNCTION); + expect(sayHelloBeforeUnload()).toBe('hello world'); + + // Act + container.unload(MODULE2); + + // Assert + const sayHelloAfterUnload = container.get(DI.SIMPLE_FUNCTION); + expect(sayHelloAfterUnload()).toBe('module 1 hello world'); + }); + }); + + describe('When no other module has this dependency already registered', () => { + it('should remove all its dependencies', () => { + // Arrange + const MY_MODULE = Symbol('myModule'); + + const module = createModule(); + module.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld); + container.load(MY_MODULE, module); + + const sayHelloBeforeUnload = container.get(DI.SIMPLE_FUNCTION); + expect(sayHelloBeforeUnload()).toBe('hello world'); + + // Act + container.unload(MY_MODULE); + + // Assert + expect(() => container.get(DI.SIMPLE_FUNCTION)) + .toThrowError(`No binding found for key: ${DI.SIMPLE_FUNCTION.toString()}`); + }); + }); + }); +}); \ No newline at end of file diff --git a/specs/scope.spec.ts b/specs/scope.spec.ts new file mode 100644 index 0000000..20cee7e --- /dev/null +++ b/specs/scope.spec.ts @@ -0,0 +1,149 @@ +import {Container, createContainer, Scope} from "../src"; +import {DI} from "./examples/DI"; +import {MyService} from "./examples/MyService"; +import {MyServiceInterface} from "./examples/MyServiceInterface"; +import {vi} from "vitest"; +import {sayHelloWorld} from "./examples/sayHelloWorld"; +import {SayHelloType} from "./examples/SayHelloType"; + +describe('Scope', () => { + + let container: Container; + let factoryCalls = vi.fn(); + + beforeEach(() => { + container = createContainer(); + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + factoryCalls = vi.fn(); + }); + + describe.each([ + {scope: undefined}, + {scope: 'singleton'}, + ])('When the scope is default or defined to "singleton"', ({scope}) => { + it('should return the same instance', () => { + // Arrange + container.bind(DI.MY_SERVICE).toFactory(() => { + factoryCalls(); + return MyService({ + dep1: container.get(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }, scope as Scope); + + const myService1 = container.get(DI.MY_SERVICE); + + // Act + const myService2 = container.get(DI.MY_SERVICE); + + // Assert + expect(myService1).toBe(myService2); + expect(factoryCalls).toHaveBeenCalledTimes(1); + }); + }); + + describe('When the scope is defined to "transient"', () => { + it('should return a new instance each time', () => { + // Arrange + container.bind(DI.MY_SERVICE).toFactory(() => { + factoryCalls(); + return MyService({ + dep1: container.get(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }, 'transient'); + + const myService1 = container.get(DI.MY_SERVICE); + + // Act + const myService2 = container.get(DI.MY_SERVICE); + + // Assert + expect(myService1).not.toBe(myService2); + expect(factoryCalls).toHaveBeenCalledTimes(2); + }); + }); + + describe('When the scope is defined to "scoped"', () => { + it('should return the same instance within the same scope', () => { + // Arrange + container.bind(DI.DEP1).toValue('dependency1'); + container.bind(DI.DEP2).toValue(42); + container.bind(DI.MY_SERVICE).toFactory(() => { + factoryCalls(); + return MyService({ + dep1: container.get(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }, 'scoped'); + + let myService1: MyServiceInterface | undefined; + let myService2: MyServiceInterface | undefined; + + // Act + container.runInScope(() => { + myService1 = container.get(DI.MY_SERVICE); + myService2 = container.get(DI.MY_SERVICE); + }); + + // Assert + expect(myService1).toBeDefined(); + expect(myService2).toBeDefined(); + expect(myService1).toBe(myService2); + expect(factoryCalls).toHaveBeenCalledTimes(1); + }); + + it('should return different instances in different scopes', () => { + // Arrange + container.bind(DI.MY_SERVICE).toFactory(() => { + factoryCalls(); + return MyService({ + dep1: container.get(DI.DEP1), + dep2: container.get(DI.DEP2) + }); + }, 'scoped'); + + let myService1: MyServiceInterface | undefined; + let myService2: MyServiceInterface | undefined; + + container.runInScope(() => { + myService1 = container.get(DI.MY_SERVICE); + }); + + // Act + container.runInScope(() => { + myService2 = container.get(DI.MY_SERVICE); + }); + + // Assert + expect(myService1).toBeDefined(); + expect(myService2).toBeDefined(); + expect(myService1).not.toBe(myService2); + expect(factoryCalls).toHaveBeenCalledTimes(2); + }); + }); + + describe('When a scoped dependency is resolved outside of a scope', () => { + it('should throw an error', () => { + // Arrange + container.bind(DI.MY_SERVICE) + .toHigherOrderFunction(MyService, {dep1: DI.DEP1, dep2: DI.DEP2}, 'scoped'); + + // Act & Assert + expect(() => container.get(DI.MY_SERVICE)) + .toThrowError(`Cannot resolve scoped binding outside of a scope: ${DI.MY_SERVICE.toString()}`); + }); + }); + + describe('When an unknown scope is used during binding', () => { + it('should throw an error', () => { + // Arrange + container.bind(DI.MY_SERVICE).toFunction(sayHelloWorld, 'unknown' as any); + + // Act & Assert + expect(() => container.get(DI.MY_SERVICE)) + .toThrowError('Unknown scope: unknown'); + }); + }); +});