From f7c4e609c2f1d9c41ce53a265da865d40539bec5 Mon Sep 17 00:00:00 2001 From: Max P Date: Fri, 16 Aug 2024 17:31:07 +0200 Subject: [PATCH] ### Raise Coverage and Enhance Testing Increased coverage thresholds to 90% across all metrics. Added exclusions for `.spec.ts` and `.test.ts` files to the coverage configuration. Introduced new test files for `Decorators` and `Functions`, incorporating detailed unit tests for decorators and DI functionalities. Removed outdated and redundant test files, consolidating their functionality into the updated tests. Also added new npm script for jest watch mode. Marked helper and main index files to be ignored by the coverage. --- jest.config.coverage.cjs | 10 +- package.json | 1 + src/__tests__/DIContainer.test.ts | 4 - src/__tests__/Decorators.spec.ts | 262 +++++++++++++++++++++++++++++ src/__tests__/Decorators.test.ts | 15 ++ src/__tests__/Functions.spec.ts | 70 ++++++++ src/__tests__/Functions.test.ts | 7 + src/__tests__/IDIContainer.spec.ts | 46 ----- src/__tests__/ITSinjex.spec.ts | 78 +++++++++ src/__tests__/TSinjex.test.ts | 4 + src/helper/ImplementsStatic.ts | 2 + src/index.ts | 2 + tsconfig.json | 3 +- 13 files changed, 448 insertions(+), 56 deletions(-) delete mode 100644 src/__tests__/DIContainer.test.ts create mode 100644 src/__tests__/Decorators.spec.ts create mode 100644 src/__tests__/Decorators.test.ts create mode 100644 src/__tests__/Functions.spec.ts create mode 100644 src/__tests__/Functions.test.ts delete mode 100644 src/__tests__/IDIContainer.spec.ts create mode 100644 src/__tests__/ITSinjex.spec.ts create mode 100644 src/__tests__/TSinjex.test.ts 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