Skip to content

Commit

Permalink
Fix token reflection
Browse files Browse the repository at this point in the history
Fixed the way tokens are reflected. Now it won't assume type is defined, and will make best effort to use token or type name.

Removed the duplicates check there because I was not sure what should it do (I can bring it back, just need to know how to integrate it correctly).

Resolves #25
  • Loading branch information
iddan committed Dec 13, 2022
1 parent fbce366 commit 63e009f
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 55 deletions.
2 changes: 1 addition & 1 deletion src/lib/reflector.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('Reflector Service TestBed', () => {

describe('scenario: successfully reflecting dependencies and tokens', () => {
describe('when not overriding any of the class dependencies', () => {
let result: Map<string | Type<unknown>, Type<unknown>>;
let result: Map<string | Type<unknown>, string | Type<unknown>>;

beforeAll(() => {
getMetadataStub.mockImplementation(VALID_IMPL);
Expand Down
72 changes: 32 additions & 40 deletions src/lib/reflector.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,46 @@ export interface CustomToken {
param: Type | { forwardRef: () => Type } | string;
}

export type TokenOrType = string | Type<unknown>;
export type ClassDependencies = Map<TokenOrType, Type<unknown>>;

export class ReflectorService {
private static readonly INJECTED_TOKENS_METADATA = 'self:paramtypes';
private static readonly PARAM_TYPES_METADATA = 'design:paramtypes';

public constructor(private readonly reflector: typeof Reflect) {}

public reflectDependencies(targetClass: Type): Map<string | Type<unknown>, Type<unknown>> {
const classDependencies = new Map<string | Type<unknown>, Type<unknown>>();
public reflectDependencies(targetClass: Type): ClassDependencies {
const classDependencies = new Map<TokenOrType, Type<unknown>>();

const types = this.reflectParamTypes(targetClass);
const tokens = this.reflectParamTokens(targetClass);

const duplicates = ReflectorService.findDuplicates([...types]).map((typeOrToken) =>
typeof typeOrToken === 'string' ? typeOrToken : typeOrToken.name
);

types.forEach((type: Type<unknown>, index: number) => {
if (type.name === 'Object' || duplicates.includes(type.name)) {
const token = ReflectorService.findToken(tokens, index);

if (!token) {
if (type.name === 'Object') {
throw new Error(
`'${targetClass.name}' is missing a token for the dependency at index [${index}], did you forget to inject it using @Inject()?`
);
} else {
throw new Error(`'${targetClass.name}' includes non-unique types/tokens dependencies`);
types.forEach((type, index) => {
const token = ReflectorService.findToken(tokens, index);
const isObjectType = type && type.name === 'Object';

if (token) {
const ref = ReflectorService.resolveRefFromToken(token);
if (isObjectType) {
if (typeof ref !== 'string') {
classDependencies.set(ref, ref);
return;
}
}

const ref = typeof token === 'object' && 'forwardRef' in token ? token.forwardRef() : token;

classDependencies.set(ref, type);
} else {
if (type) {
classDependencies.set(ref, type);
return;
}
}
if (type && !isObjectType) {
classDependencies.set(type, type);
return;
}

throw new Error(
`'${targetClass.name}' is missing a token for the dependency at index [${index}], did you forget to inject it using @Inject()?`
);
});

return classDependencies;
Expand All @@ -50,31 +54,19 @@ export class ReflectorService {
return this.reflector.getMetadata(ReflectorService.INJECTED_TOKENS_METADATA, targetClass) || [];
}

private reflectParamTypes(targetClass: Type): Type[] {
private reflectParamTypes(targetClass: Type): Array<Type | undefined> {
return this.reflector.getMetadata(ReflectorService.PARAM_TYPES_METADATA, targetClass) || [];
}

private static findToken(
list: CustomToken[],
index: number
): Type | { forwardRef: () => Type } | string | undefined {
private static findToken(list: CustomToken[], index: number): Token | undefined {
const record = list.find((element) => element.index === index);

return record?.param;
}

private static findDuplicates(typesOrToken: (Type | string)[]) {
const items = [...typesOrToken.sort()];

let index = items.length;
const duplicates = [];

while (index--) {
items[index] === items[index - 1] &&
duplicates.indexOf(items[index]) == -1 &&
duplicates.push(items[index]);
}

return duplicates;
private static resolveRefFromToken(token: Token): string | Type<any> {
return typeof token === 'object' && 'forwardRef' in token ? token.forwardRef() : token;
}
}

type Token = Type | { forwardRef: () => Type } | string;
4 changes: 2 additions & 2 deletions src/lib/test-bed-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'reflect-metadata';
import { DeepPartial } from 'ts-essentials';
import { MockFunction, Override, UnitTestBed, Type } from './types';
import { MockResolver } from './mock-resolver';
import { ReflectorService } from './reflector.service';
import { ReflectorService, TokenOrType } from './reflector.service';

import Mocked = jest.Mocked;

Expand All @@ -28,7 +28,7 @@ export interface TestBedResolver<TClass = any> {
}

export class TestBedResolver<TClass = any> {
private readonly dependencies = new Map<Type | string, DeepPartial<unknown>>();
private readonly dependencies = new Map<TokenOrType, Type<unknown>>();
private readonly depNamesToMocks = new Map<Type | string, Mocked<any>>();

public constructor(
Expand Down
26 changes: 14 additions & 12 deletions test/automock-jest-nestjs-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ describe('AutoMock NestJS E2E Test', () => {
let unitResolver: TestBedResolver<NestJSTestClass>;

describe('given a unit testing builder with two overrides', () => {
const loggerMock = {
log() {
return 'baz-from-test';
},
};
const testClassOneMock: { foo?: ((flag: boolean) => Promise<string>) | undefined; } = {
async foo(): Promise<string> {
return 'foo-from-test';
},
};
beforeAll(() => {
unitResolver = TestBed.create<NestJSTestClass>(NestJSTestClass)
.mock(TestClassOne)
.using({
async foo(): Promise<string> {
return 'foo-from-test';
},
})
.using(testClassOneMock)
.mock<Logger>('LOGGER')
.using({
log() {
return 'baz-from-test';
},
});
.using(loggerMock);
});

describe('when compiling the builder and turning into testing unit', () => {
Expand All @@ -42,11 +44,11 @@ describe('AutoMock NestJS E2E Test', () => {
test('then successfully resolve the dependencies of the tested classes', () => {
const { unitRef } = unit;

expect(unitRef.get(TestClassOne)).toBeDefined();
expect(unitRef.get(TestClassOne).foo).toBe(testClassOneMock.foo);
expect(unitRef.get(TestClassTwo)).toBeDefined();
expect(unitRef.get(getRepositoryToken(Foo) as string)).toBeDefined();
expect(unitRef.get(getRepositoryToken(Bar) as string)).toBeDefined();
expect(unitRef.get('LOGGER')).toBeDefined();
expect(unitRef.get<{ log: () => void }>('LOGGER').log).toBe(loggerMock.log);
expect(unitRef.get(TestClassThree)).toBeDefined();
});

Expand Down

0 comments on commit 63e009f

Please sign in to comment.