Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(parser): avoid circular mocks #137

Open
wants to merge 2 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MockFactory e2e Test Scenario: circular mocks when when I create a factory with circular refernse then do have something 1`] = `
AnotherBird {
"name": "AnotherBirdyBird",
"nest": BirdNest {
"bird": Bird {
"birthday": 2000-01-01T00:00:00.000Z,
"name": "BirdyBird",
"owner": Person {
"car": Car {
"model": "BMW",
},
"gender": "male",
},
},
"location": "location",
},
}
`;

exports[`MockFactory e2e Test Scenario: using a plain object when I create 3 birds and covert them into a plain object then return a nested plain object 1`] = `
Array [
Object {
Expand Down
20 changes: 18 additions & 2 deletions packages/mockingbird/test/e2e/mock-factory.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { MockFactory } from '../../src';
import { TestClassesE2E } from './test-classes';
import * as faker from 'faker';
import { TestClassesE2E } from './test-classes';
import { MockFactory } from '../../src';

import Bird = TestClassesE2E.Bird;
import AnotherBird = TestClassesE2E.AnotherBird;

describe('MockFactory e2e Test', () => {
let result;
Expand Down Expand Up @@ -89,4 +91,18 @@ describe('MockFactory e2e Test', () => {
});
});
});

scenario('circular mocks', () => {
let mock;

when('when I create a factory with circular refernse', () => {
beforeAll(() => {
mock = MockFactory(AnotherBird).one();
});

then('do have something', () => {
expect(mock).toMatchSnapshot();
});
});
});
});
16 changes: 16 additions & 0 deletions packages/mockingbird/test/e2e/test-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,20 @@ export namespace TestClassesE2E {
@Mock()
birthday: Date;
}

export class AnotherBird {
@Mock('AnotherBirdyBird')
name: string;

@Mock(() => BirdNest)
nest: unknown;
}

export class BirdNest {
@Mock('location')
location: string;

@Mock(() => Bird)
bird: Bird;
}
}
8 changes: 5 additions & 3 deletions packages/parser/src/lib/handlers/class-callback-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ export class ClassCallbackHandler implements ValueHandler {
return property.propertyValue.isClassCb();
}

public produceValue<TClass>(property: Property): TClass {
const analyzer = Container.get<ClassParser>(ClassParser);
public produceValue<TClass>(property: Property, config: { reference: string }): TClass | null {
const parser = Container.get<ClassParser>(ClassParser);

return analyzer.parse((property.propertyValue.decorator.value as LazyType<TClass>)());
return parser.parse((property.propertyValue.decorator.value as LazyType<TClass>)(), {
reference: config.reference,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('SingleClassValueHandler Unit', () => {

describe("when calling 'produceValue' method", () => {
test("then call 'parse' with the given class", () => {
expect(handler.produceValue(property)).toBeInstanceOf(DTO_CLASS_VALUE);
expect(handler.produceValue(property, { reference: undefined })).toBeInstanceOf(DTO_CLASS_VALUE);
});
});
});
Expand Down
31 changes: 26 additions & 5 deletions packages/parser/src/lib/parser/class-parser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Container, Inject, Service } from 'typedi';
import { ClassReflector, Property } from '@mockingbird/reflect';
import { Class, Faker } from '@mockingbird/common';
import { ClassPropsReflection, ClassReflector, Property } from '@mockingbird/reflect';
import { Class, Faker, LazyType } from '@mockingbird/common';
import { MutationsCallback, ParserConfig, ParsingStrategy } from '../types/types';
import { ValueHandler } from '../types/value-handler.interface';
import { EnumValueHandler } from '../handlers/enum-value-handler';
Expand All @@ -25,20 +25,32 @@ export class ClassParser<TClass = any> {

public constructor(@Inject('Faker') private readonly faker: Faker) {}

private handlePropertyValue(property: Property): TClass | TClass[] {
private static isClassIncludesClassName(prop: Property, ctorName: string) {
return (prop.propertyValue.decorator.value as LazyType)().name === ctorName;
}

private handlePropertyValue(property: Property, reference: string = undefined): TClass | TClass[] {
for (const classHandler of this.valueHandlers) {
const handler = Container.get<ValueHandler>(classHandler);

if (handler.shouldHandle(property)) {
if (classHandler.name === ClassCallbackHandler.name) {
if (reference && ClassParser.isClassIncludesClassName(property, reference)) {
return undefined;
}

return handler.produceValue<TClass>(property, { reference });
}

return handler.produceValue<TClass>(property);
}
}
}

public parse(targetClass: Class<TClass>, config: ParserConfig<TClass> = {}): TClass {
const classReflection = ClassReflector.getInstance().reflectClass(targetClass);

const { omit = [], pick = [] } = config;

let { mutations = {} } = config;
let strategy: ParsingStrategy;

Expand Down Expand Up @@ -75,7 +87,16 @@ export class ClassParser<TClass = any> {
return acc;
}

return { ...acc, [property.name]: value || this.handlePropertyValue(property) };
const propFinalValue = value || this.handlePropertyValue(property, config.reference ?? targetClass.name);

if (!propFinalValue) {
return acc;
}

return {
[property.name]: propFinalValue,
...acc,
};
};

const derivedProps = classReflection.reduce(deriveFromProps, {});
Expand Down
1 change: 1 addition & 0 deletions packages/parser/src/lib/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ParserConfig<TClass> {
mutations?: OptionalClassValues<TClass> | MutationsCallback<TClass>;
omit?: (keyof TClass)[];
pick?: (keyof TClass)[];
reference?: string;
}

export type ParsingStrategy = 'pick' | 'omit' | undefined;
2 changes: 1 addition & 1 deletion packages/parser/src/lib/types/value-handler.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { Property } from '@mockingbird/reflect';

export interface ValueHandler {
shouldHandle(property: Property): boolean;
produceValue<T>(property: Property): T | T[];
produceValue<T>(property: Property, config?: Record<string, any>): T | T[];
}
17 changes: 16 additions & 1 deletion packages/parser/test/integration/common/test-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Mock } from '@mockingbird/reflect';
enum TestEnum {
Foo = 'foo',
Bar = 111,
Bazz = 'Bazz1234',
}

export namespace TestClasses {
Expand Down Expand Up @@ -86,4 +85,20 @@ export namespace TestClasses {
@Mock(/^[a-z]{4,5}$/)
prop3: string;
}

export class InverseTestClassOne {
@Mock()
title: string;

@Mock(() => InverseTestClassTwo)
classTwo: any;
}

export class InverseTestClassTwo {
@Mock()
name: string;

@Mock(() => InverseTestClassOne)
classOne: any;
}
}
24 changes: 23 additions & 1 deletion packages/parser/test/integration/mock-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import TestClassWithEnum = TestClasses.TestClassWithEnum;
import TestClassWithOtherClass = TestClasses.TestClassWithSingleClass;
import TestClassWithMultiClass = TestClasses.TestClassWithArrayOfClasses;
import TestClassWithRegex = TestClasses.TestClassWithRegex;
import InverseTestClassOne = TestClasses.InverseTestClassOne;
import InverseTestClassTwo = TestClasses.InverseTestClassTwo;

describe('MockGenerator - Integration Test', () => {
let result;
Expand Down Expand Up @@ -89,7 +91,7 @@ describe('MockGenerator - Integration Test', () => {
});

test('then return an object with the given class', () => {
expect(Object.keys(result.dog)).toEqual(['name', 'points']);
expect(Object.keys(result.dog)).toEqual(['points', 'name']);
});
});

Expand Down Expand Up @@ -132,5 +134,25 @@ describe('MockGenerator - Integration Test', () => {
expect(result).toHaveLength(4);
});
});

describe('mock with circular/inverse class', () => {
beforeAll(() => {
result = mockGenerator.generate(InverseTestClassOne);
});

test("then return array with length of 'count'", () => {
expect(result.classTwo).not.toHaveProperty('classOne');
});
});

describe('mock with circular/inverse class', () => {
beforeAll(() => {
result = mockGenerator.generate(InverseTestClassTwo);
});

test("then return array with length of 'count'", () => {
expect(result.classOne).not.toHaveProperty('classTwo');
});
});
});
});