diff --git a/jest.config.coverage.cjs b/jest.config.coverage.cjs index df0c2a9..b01b759 100644 --- a/jest.config.coverage.cjs +++ b/jest.config.coverage.cjs @@ -15,14 +15,16 @@ module.exports = { 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/*.performance.test.ts', + '!src/**/*.spec.ts', + '!src/**/*.test.ts', '!src/auto-imports.ts' ], coverageThreshold: { global: { - branches: 70, - functions: 70, - lines: 70, - statements: 70, + branches: 90, + functions: 90, + lines: 90, + statements: 90, }, }, }; diff --git a/package.json b/package.json index 1201b35..53e7338 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "eslint --ext .ts .", "lint:fix": "eslint --fix --ext .ts .", "test": "jest", + "test:watch": "jest --watch --onlyChanged", "test:file": "jest --watch --onlyChanged --coverage=true --verbose", "test:verbose": "jest --verbose", "test:coverage": "jest --config jest.config.coverage.cjs --coverage", diff --git a/src/__tests__/DIContainer.test.ts b/src/__tests__/DIContainer.test.ts deleted file mode 100644 index 556c6a2..0000000 --- a/src/__tests__/DIContainer.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { test_IDIContainer } from './IDIContainer.spec'; -import { TSinjex } from '../classes/TSinjex'; - -test_IDIContainer(TSinjex); diff --git a/src/__tests__/Decorators.spec.ts b/src/__tests__/Decorators.spec.ts new file mode 100644 index 0000000..1637ead --- /dev/null +++ b/src/__tests__/Decorators.spec.ts @@ -0,0 +1,262 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Inject } from 'src/decorators/Inject'; +import { DependencyResolutionError } from 'src/interfaces/Exceptions'; +import { ForceConstructor } from 'src/types/GenericContructor'; +import { ITSinjex_, ITSinjex } from '../interfaces/ITSinjex'; + +/** + * Test the Inject decorator. + * @param Container The implementation to test. + * @param inject The Inject decorator to test. + */ +export function test_InjectDecorator( + Container: ITSinjex_, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + inject: Function, +): void { + describe('Inject Decorator Tests', () => { + let container: ITSinjex; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_instance'] = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_dependencies'] = undefined; + container = Container.getInstance(); + }); + + it('should inject dependency when necessary is true', () => { + container.register('MockDependencyIdentifier', { + value: 'test-value', + }); + + class TestClass { + @Inject('MockDependencyIdentifier') + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + } + + const instance = new TestClass(); + expect(instance.getDependency().value).toBe('test-value'); + }); + + it('should inject dependency and run initializer', () => { + container.register('MockDependencyIdentifier', { + value: 'test-value', + }); + + class TestClass { + @Inject('MockDependencyIdentifier', (x: string) => { + (x as unknown as { value: string }).value = + 'test-value-init'; + + return x; + }) + dependency!: any; + + public getDependency() { + return this.dependency; + } + } + + const instance = new TestClass(); + expect(instance.getDependency().value).toBe('test-value-init'); + }); + + it('should throw an error when necessary is true and the initializer throws an error', () => { + let _error: Error | undefined = undefined; + + container.register('InitThrowDependencie', { + value: 'test-value', + }); + + try { + class TestClass { + @Inject( + 'InitThrowDependencie', + () => { + throw new Error('Initializer error'); + }, + true, + ) + dependency!: any; + + public getDependency() { + return this.dependency; + } + } + + const _instance = new TestClass(); + console.log(_instance.getDependency()); + } catch (error) { + _error = error; + } + expect(_error).toBeInstanceOf(Error); + }); + + it('should throw an error when necessary is true and dependency is not found', () => { + let _error: Error | undefined = undefined; + + try { + class TestClass { + @Inject('NonExistentDependencyIdentifier') + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + } + + const _instance = new TestClass(); + console.log(_instance.getDependency()); + } catch (error) { + _error = error; + } + expect(_error).toBeInstanceOf(DependencyResolutionError); + }); + }); +} + +export function test_RegisterDecorator( + Container: ITSinjex_, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + register: Function, +): void { + describe('Register Decorator Tests', () => { + let container: ITSinjex; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_instance'] = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_dependencies'] = undefined; + container = Container.getInstance(); + }); + + it('should register a dependency', () => { + @register('MockDependencyIdentifier') + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + } + + expect(container.resolve('MockDependencyIdentifier')).toBe( + TestClass, + ); + }); + }); +} + +export function test_RegisterInstanceDecorator( + Container: ITSinjex_, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + registerInstance: Function, +): void { + describe('RegisterInstance Decorator Tests', () => { + let container: ITSinjex; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_instance'] = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_dependencies'] = undefined; + container = Container.getInstance(); + }); + + it('should register an instance of a dependency', () => { + @registerInstance('InstanceIdentifier') + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + + public mark: string = 'instance'; + } + + expect( + container.resolve('InstanceIdentifier').mark, + ).toBe('instance'); + }); + + it('should register an instance of a dependency an run the init function', () => { + @registerInstance( + 'InstanceIdentifier', + (x: ForceConstructor) => { + const instance = new x(); + instance.mark = 'init'; + + return instance; + }, + ) + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + + public mark: string = 'instance'; + } + + expect( + container.resolve('InstanceIdentifier').mark, + ).toBe('init'); + }); + + it('should register an instance of a dependency and get it on set', () => { + @registerInstance('InstanceIdentifier') + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + + public mark: string = 'instance'; + public test: string = 'test'; + } + + container.resolve('InstanceIdentifier').test = 'test2'; + + expect( + container.resolve('InstanceIdentifier').test, + ).toBe('test2'); + }); + + it('should register an instance of a dependency an run the init function on set', () => { + @registerInstance( + 'InstanceIdentifier', + (x: ForceConstructor) => { + const instance = new x(); + instance.mark = 'init'; + + return instance; + }, + ) + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + + public mark: string = 'instance'; + public test: string = 'test'; + } + + container.resolve('InstanceIdentifier').test = 'test2'; + + expect( + container.resolve('InstanceIdentifier').test, + ).toBe('test2'); + }); + }); +} diff --git a/src/__tests__/Decorators.test.ts b/src/__tests__/Decorators.test.ts new file mode 100644 index 0000000..efba3e6 --- /dev/null +++ b/src/__tests__/Decorators.test.ts @@ -0,0 +1,15 @@ +import { TSinjex } from 'src/classes/TSinjex'; +import { Inject } from 'src/decorators/Inject'; +import { Register } from 'src/decorators/Register'; +import { RegisterInstance } from 'src/decorators/RegisterInstance'; +import { + test_InjectDecorator, + test_RegisterDecorator, + test_RegisterInstanceDecorator, +} from './Decorators.spec'; + +test_InjectDecorator(TSinjex, Inject); + +test_RegisterDecorator(TSinjex, Register); + +test_RegisterInstanceDecorator(TSinjex, RegisterInstance); diff --git a/src/__tests__/Functions.spec.ts b/src/__tests__/Functions.spec.ts new file mode 100644 index 0000000..00d40a0 --- /dev/null +++ b/src/__tests__/Functions.spec.ts @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ITSinjex, ITSinjex_ } from 'src/interfaces/ITSinjex'; + +export function test_RegisterFunction( + Container: ITSinjex_, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + register: Function, +): void { + describe('Register Function Tests', () => { + let container: ITSinjex; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_instance'] = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_dependencies'] = undefined; + container = Container.getInstance(); + }); + + it('should register a dependency', () => { + const identifier = 'MockDependencyIdentifier'; + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + } + + register(identifier, TestClass, false); + + const resolvedDependency = container.resolve(identifier); + expect(resolvedDependency).toBe(TestClass); + }); + }); +} + +export function test_ResolveFunction( + Container: ITSinjex_, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + resolve: Function, +): void { + describe('Resolve Function Tests', () => { + let container: ITSinjex; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_instance'] = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_dependencies'] = undefined; + container = Container.getInstance(); + }); + + it('should resolve a dependency', () => { + const identifier = 'MockDependencyIdentifier'; + class TestClass { + private readonly _dependency!: any; + + public getDependency() { + return this._dependency; + } + } + + container.register(identifier, TestClass); + + const resolvedDependency = resolve(identifier); + expect(resolvedDependency).toBe(TestClass); + }); + }); +} diff --git a/src/__tests__/Functions.test.ts b/src/__tests__/Functions.test.ts new file mode 100644 index 0000000..1f89810 --- /dev/null +++ b/src/__tests__/Functions.test.ts @@ -0,0 +1,7 @@ +import { TSinjex } from 'src/classes/TSinjex'; +import { register } from 'src/functions/register'; +import { resolve } from 'src/functions/resolve'; +import { test_RegisterFunction, test_ResolveFunction } from './Functions.spec'; + +test_RegisterFunction(TSinjex, register); +test_ResolveFunction(TSinjex, resolve); diff --git a/src/__tests__/IDIContainer.spec.ts b/src/__tests__/IDIContainer.spec.ts deleted file mode 100644 index bc5c40f..0000000 --- a/src/__tests__/IDIContainer.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ITSinjex_, ITSinjex } from '../interfaces/ITSinjex'; - -/** - * Test the implementation of a DIContainer - * @param Container The DIContainer implementation to test. - * Must implement {@link ITSinjex}, {@link ITSinjex_} - */ -export function test_IDIContainer(Container: ITSinjex_): void { - describe('IDIContainer Implementation Tests', () => { - let container: ITSinjex; - - beforeEach(() => { - container = Container.getInstance(); - }); - - it('should register and resolve a dependency', () => { - const identifier = 'myDependency'; - const dependency = { value: 42 }; - - container.register(identifier, dependency); - - const resolvedDependency = - container.resolve(identifier); - expect(resolvedDependency).toBe(dependency); - }); - - it('should register and resolve a dependency static', () => { - const identifier = 'myDependency'; - const dependency = { value: 42 }; - - Container.register(identifier, dependency); - - const resolvedDependency = - Container.resolve(identifier); - expect(resolvedDependency).toBe(dependency); - }); - - it('should throw an error when resolving a non-registered dependency static', () => { - const identifier = 'nonExistentDependency'; - - expect(() => Container.resolve(identifier)).toThrow(); - }); - - // Add more tests as necessary - }); -} diff --git a/src/__tests__/ITSinjex.spec.ts b/src/__tests__/ITSinjex.spec.ts new file mode 100644 index 0000000..8d1f979 --- /dev/null +++ b/src/__tests__/ITSinjex.spec.ts @@ -0,0 +1,78 @@ +import { ITSinjex_, ITSinjex } from '../interfaces/ITSinjex'; + +/** + * Test the implementation of the `ITSinjex` interface. + * @param Container The implementation to test. + * Must implement {@link ITSinjex}, {@link ITSinjex_} + */ +export function test_ITSinjex(Container: ITSinjex_): void { + describe('IDIContainer Implementation Tests', () => { + let container: ITSinjex; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_instance'] = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (Container as any)['_dependencies'] = undefined; + container = Container.getInstance(); + }); + + it('should register and resolve a dependency', () => { + const identifier = 'myDependency'; + const dependency = { value: 42 }; + + container.register(identifier, dependency); + + const resolvedDependency = + container.resolve(identifier); + expect(resolvedDependency).toBe(dependency); + }); + + it('should register and resolve a dependency static', () => { + const identifier = 'myDependency'; + const dependency = { value: 42 }; + + Container.register(identifier, dependency); + + const resolvedDependency = + Container.resolve(identifier); + expect(resolvedDependency).toBe(dependency); + }); + + it('should throw an error when resolving a non-registered dependency static', () => { + const identifier = 'nonExistentDependency'; + + expect(() => Container.resolve(identifier)).toThrow(); + }); + + it('should return undefined when resolving a non-registered, non-necessary dependency', () => { + const resolvedDependency = Container.resolve( + 'nonExistentDependency', + false, + ); + expect(resolvedDependency).toBe(undefined); + }); + + it('should warn when resolving a deprecated dependency', () => { + const identifier = 'deprecatedDependency'; + const dependency = { value: 42 }; + + // Spy on console.warn + const warnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + Container.register(identifier, dependency, true); + + const resolvedDependency = + Container.resolve(identifier); + expect(resolvedDependency).toBe(dependency); + + // Expect console.warn to be called + expect(warnSpy).toHaveBeenCalled(); + + // Restore the original console.warn + warnSpy.mockRestore(); + }); + }); +} diff --git a/src/__tests__/TSinjex.test.ts b/src/__tests__/TSinjex.test.ts new file mode 100644 index 0000000..0763003 --- /dev/null +++ b/src/__tests__/TSinjex.test.ts @@ -0,0 +1,4 @@ +import { test_ITSinjex } from './ITSinjex.spec'; +import { TSinjex } from '../classes/TSinjex'; + +test_ITSinjex(TSinjex); diff --git a/src/helper/ImplementsStatic.ts b/src/helper/ImplementsStatic.ts index a976c31..8375afb 100644 --- a/src/helper/ImplementsStatic.ts +++ b/src/helper/ImplementsStatic.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + /** * Decorator to enforce static implementation of an interface. * Warns on compile time if the interface is not implemented. diff --git a/src/index.ts b/src/index.ts index bfe2716..12804a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + // Main export * from './classes/TSinjex'; diff --git a/tsconfig.json b/tsconfig.json index 65130fe..782ed0e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,7 +37,6 @@ "src/**/*.ts" ], "exclude": [ - "node_modules", - "**/*.spec.ts" + "node_modules" ] } \ No newline at end of file