Skip to content

Commit

Permalink
refactor(mockingbird-ts): arrange classes - change mock-factory to mo…
Browse files Browse the repository at this point in the history
…ck-generator (#55)

* refactor(mockingbird-ts): change the name of mock factory to be mock generator

change MockFactory name to be MockGenerator

BREAKING CHANGE: MockFactory changed to be MockGenerator

re #42

* chore(repo): update tsconfig build config

* refactor(types): change 'class' to 'type' with new definition

* refactor(reflect): change 'class' type to 'type' type

* refactor(parser): change 'class' type to 'type'

* refactor(mockingbird-ts): generator now has state and di, remove static method 'create'

* test(mockingbird-ts): add unit test for mock generator

* test(mockingbird-ts): add unit test for mock generator

* refactor(parser): add option to set locale and change static class member (state instead)

* test(mockingbird-ts): remove test of circular mock
  • Loading branch information
omermorad committed Jul 18, 2021
1 parent 3239064 commit cf56561
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 118 deletions.
69 changes: 42 additions & 27 deletions packages/generator/src/lib/mock-generator.test.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
import { ClassParser } from '@mockinbird/parser';
import { MockGenerator } from './mock-generator';

const processMock = jest.fn();

jest.mock('@mockinbird/parser', () => ({
ClassParser: jest.fn().mockImplementation(() => {
return { parse: processMock };
}),
}));

describe('MockGenerator - Unit', () => {
describe('given a Mock Factory', () => {
afterEach(() => {
processMock.mockClear();
/**
* The full test of MockGenerator can be found under the 'test' folder,
* you can find there the full integration test
*/
describe('MockGenerator', () => {
describe('given a MockGenerator', () => {
let generator: MockGenerator;

const parserMock = {
setFakerLocale: jest.fn(),
parse: jest.fn(),
} as unknown as ClassParser<any>;

beforeAll(() => {
generator = new MockGenerator(parserMock);
});

class TestClass {}

describe("when calling 'create' method without options", () => {
test('then call process exactly once', () => {
MockGenerator.create(TestClass);

expect(processMock).toHaveBeenCalledTimes(1);
expect(processMock).toHaveBeenCalledWith(TestClass);
describe("when calling 'create' method", () => {
describe('with no options at all', () => {
test('then setup parser with the default locale', () => {
generator.create(class TestClass {});
expect(parserMock.setFakerLocale).toHaveBeenCalledWith('en');
});
});
});

describe("when calling 'create' method with count = 3", () => {
const count = 3;

test('then call process 3 times ', () => {
MockGenerator.create(TestClass, { count });
describe('with a given locale (as argument)', () => {
test('then setup the parser with the given locale', () => {
generator.create(class TestClass {}, 'arbitrary-locale');
expect(parserMock.setFakerLocale).toHaveBeenCalledWith('arbitrary-locale');
});
});

expect(processMock).toHaveBeenCalledTimes(count);
describe('with an object including options', () => {
describe("and the options including only 'count'", function () {
test("then call the parser 'count' times", () => {
generator.create(class TestClass {}, { count: 3 });
expect(parserMock.setFakerLocale).toHaveBeenCalledTimes(3);
});
});

describe("and the options including both 'count' and 'locale", function () {
test('then setup the parser with the locale from the options', () => {
generator.create(class TestClass {}, { count: 1, locale: 'arbitrary-locale' });
expect(parserMock.setFakerLocale).toHaveBeenCalledWith('arbitrary-locale');
});
});
});
});
});
Expand Down
38 changes: 22 additions & 16 deletions packages/generator/src/lib/mock-generator.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { Class, Faker } from '@mockinbird/types';
import { Type } from '@mockinbird/types';
import { ClassParser } from '@mockinbird/parser';
import { ClassReflector } from '@mockinbird/reflect';
import { MockDecoratorFactoryOptions } from '../types/mock-decorator-factory-options.interface';

export class MockGenerator {
export class MockGenerator<TClass = any> {
private static readonly DEFAULT_LOCALE = 'en';

public constructor(private readonly classParser: ClassParser<TClass>) {}

/**
* Return an object with all the properties decorated by the 'Mock' Decorator
*
* @example
* class Person { @Mock() name: string }
* MockGenerator.create(Person) will return an object { name: <random-string> }
*
* @param target
* @param targetClass
* @param locale
*/
public static create<TClass = any>(target: Class<TClass>): TClass;
public create(targetClass: Type<TClass>, locale?: string): TClass;

/**
* Return an array of objects with all the properties decorated by the
Expand All @@ -30,10 +32,10 @@ export class MockGenerator {
* Passing a 'locale' property will set a different locale for faker calls
* The default locale is 'en' (english)
*
* @param target
* @param targetClass
* @param options
*/
public static create<TClass = any>(target: Class<TClass>, options: MockDecoratorFactoryOptions): TClass[];
public create(targetClass: Type<TClass>, options: MockDecoratorFactoryOptions): TClass[];

/**
* Return one or many objects (array) with all the properties decorated
Expand All @@ -42,24 +44,28 @@ export class MockGenerator {
* @param targetClass
* @param options
*/
public static create<TClass = any>(
targetClass: Class<TClass>,
options?: MockDecoratorFactoryOptions
): TClass | TClass[] {
const { count = 1, locale = this.DEFAULT_LOCALE } = options || {};
public create(targetClass: Type<TClass>, options?: MockDecoratorFactoryOptions | string): TClass | TClass[] {
let locale: string;

if (typeof options === 'string') {
locale = options;
} else {
locale = options?.locale || MockGenerator.DEFAULT_LOCALE;
}

Faker.setLocale(locale);
this.classParser.setFakerLocale(locale);

const parser = new ClassParser<TClass>(Faker, new ClassReflector());
const { count = 1 } = (options || {}) as MockDecoratorFactoryOptions;

if (!count || count === 1) {
return parser.parse(targetClass);
return this.classParser.parse(targetClass);
}

const classInstances: TClass[] = [];

for (let i = 1; i <= count; i++) {
classInstances.push(parser.parse(targetClass));
const parsedClass = this.classParser.parse(targetClass);
classInstances.push(parsedClass);
}

return classInstances;
Expand Down
21 changes: 14 additions & 7 deletions packages/generator/test/integration/mock-generator.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ClassParser } from '@mockinbird/parser';
import { ClassReflector } from '@mockinbird/reflect';
import { Faker } from '@mockinbird/types';
import { TestClasses } from './common/test-classes';
import { MockGenerator } from '../../src';

Expand All @@ -11,10 +14,14 @@ import TestClassWithMultiClass = TestClasses.TestClassWithMultiClass;
describe('MockGenerator - Integration Test', () => {
let result;

const reflector = new ClassReflector();
const parser = new ClassParser(Faker, reflector);
const generator = new MockGenerator(parser);

describe('given a decorated class', () => {
describe('when using the @Mock decorator with absolute values', () => {
beforeAll(() => {
result = MockGenerator.create(TestClassWithAbsoluteValues);
result = generator.create(TestClassWithAbsoluteValues);
});

test('then return the exact same values passed in the options', () => {
Expand All @@ -26,7 +33,7 @@ describe('MockGenerator - Integration Test', () => {

describe('when using the @Mock decorator with a callback (faker)', () => {
beforeAll(() => {
result = MockGenerator.create(TestClassWithCallback);
result = generator.create(TestClassWithCallback);
});

test('then return random values from faker', () => {
Expand All @@ -39,7 +46,7 @@ describe('MockGenerator - Integration Test', () => {

describe('when using the @Mock decorator with an enum decoratorValue', () => {
beforeAll(() => {
result = MockGenerator.create(TestClassWithEnum);
result = generator.create(TestClassWithEnum);
});

test('then return one random decoratorValue (not key)', () => {
Expand All @@ -49,7 +56,7 @@ describe('MockGenerator - Integration Test', () => {

describe('when using the @Mock decorator with no/empty values', () => {
beforeAll(() => {
result = MockGenerator.create(TestClassWithNoValues);
result = generator.create(TestClassWithNoValues);
});

test('then infer the decoratorValue from the type itself', () => {
Expand All @@ -64,7 +71,7 @@ describe('MockGenerator - Integration Test', () => {

describe('when using the @Mock decorator with a single class', () => {
beforeAll(() => {
result = MockGenerator.create(TestClassWithOtherClass);
result = generator.create(TestClassWithOtherClass);
});

test('then return an object with the given class', () => {
Expand All @@ -74,7 +81,7 @@ describe('MockGenerator - Integration Test', () => {

describe('when using the @Mock decorator with a multi class', () => {
beforeAll(() => {
result = MockGenerator.create(TestClassWithMultiClass);
result = generator.create(TestClassWithMultiClass);
});

test("then return contain a property 'dogs' which is array of Dog with length of 'count'", () => {
Expand All @@ -91,7 +98,7 @@ describe('MockGenerator - Integration Test', () => {

describe("when using the @Mock decorator with 'count' option", () => {
beforeAll(() => {
result = MockGenerator.create(TestClassWithAbsoluteValues, { count: 4, locale: 'ja' });
result = generator.create(TestClassWithAbsoluteValues, { count: 4, locale: 'ja' });
});

test("then return array with length of 'count'", () => {
Expand Down
30 changes: 0 additions & 30 deletions packages/generator/test/mock-factory-circular.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/parser/src/handlers/abstract-value-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Class, Faker } from '@mockinbird/types';
import { Type, Faker } from '@mockinbird/types';
import { ClassParser } from '../lib/class-parser';

export class AbstractValueHandler {
public constructor(protected readonly faker?: Faker, protected readonly classParser?: ClassParser<Class>) {}
public constructor(protected readonly faker?: Faker, protected readonly classParser?: ClassParser<Type>) {}
}
4 changes: 2 additions & 2 deletions packages/parser/src/handlers/array-value-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Property, PropertyDecoratorValue } from '@mockinbird/reflect';
import { Class, Faker, MultiClass } from '@mockinbird/types';
import { Type, Faker, MultiClass } from '@mockinbird/types';
import { ArrayValueHandler } from './array-value-handler';
import { ClassParser } from '../lib/class-parser';

Expand Down Expand Up @@ -68,7 +68,7 @@ describe('ArrayValueHandler Unit Test', () => {
});

test('then return an array of String(s) only', () => {
const constructorIsString = (item) => (item as Class<string>).constructor.name === 'String';
const constructorIsString = (item) => (item as Type<string>).constructor.name === 'String';
expect(result.every(constructorIsString)).toBeTruthy();
});
});
Expand Down
4 changes: 2 additions & 2 deletions packages/parser/src/handlers/single-class-value-handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Property } from '@mockinbird/reflect';
import { Class } from '@mockinbird/types';
import { Type } from '@mockinbird/types';
import { AbstractValueHandler } from './abstract-value-handler';
import { ValueHandler } from '../types/value-handler.interface';
import { isPrimitive } from '../common/is-primitive';
Expand All @@ -10,6 +10,6 @@ export class SingleClassValueHandler extends AbstractValueHandler implements Val
}

public produceValue(propertyDto: Property): any {
return this.classParser.parse(propertyDto.decoratorValue.value as Class);
return this.classParser.parse(propertyDto.decoratorValue.value as Type);
}
}
25 changes: 15 additions & 10 deletions packages/parser/src/lib/class-parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Property, ClassReflector } from '@mockinbird/reflect';
import { Class, Faker } from '@mockinbird/types';
import { Type, Faker } from '@mockinbird/types';
import { CallbackValueHandler } from '../handlers/callback-value-handler';
import { ObjectLiteralValueHandler } from '../handlers/object-literal-value-handler';
import { EnumValueHandler } from '../handlers/enum-value-handler';
Expand All @@ -8,12 +8,13 @@ import { SingleClassValueHandler } from '../handlers/single-class-value-handler'
import { PrimitiveValueHandler } from '../handlers/primitive-value-handler';
import { ValueHandler } from '../types/value-handler.interface';

export interface ClassParser<T> {
parse(target: Class<T>): T;
export interface ClassParser<TClass> {
parse(target: Type<TClass>): TClass;
setFakerLocale(locale: Faker['locale']): void;
}

export class ClassParser<T> {
private static readonly VALUE_HANDLERS: Class<ValueHandler>[] = [
export class ClassParser<TClass> {
private readonly valueHandlers: Type<ValueHandler>[] = [
EnumValueHandler,
ArrayValueHandler,
SingleClassValueHandler,
Expand All @@ -24,29 +25,33 @@ export class ClassParser<T> {

public constructor(private readonly faker: Faker, private readonly reflector: ClassReflector) {}

private handlePropertyValue(property: Property): T | T[] {
for (const classHandler of ClassParser.VALUE_HANDLERS) {
private handlePropertyValue(property: Property): TClass | TClass[] {
for (const classHandler of this.valueHandlers) {
const handler = new classHandler(this.faker, this);

if (handler.shouldHandle(property)) {
return handler.produceValue<T>(property);
return handler.produceValue<TClass>(property);
}
}
}

public setFakerLocale(locale: Faker['locale']): void {
this.faker.setLocale(locale);
}

/**
* Return an object from the target class with all the properties
* decorated by the 'Mock' Decorator
*
* @param targetClass
*/
public parse(targetClass: Class<T>): T {
public parse(targetClass: Type<TClass>): TClass {
if (!targetClass) {
throw new Error(`Target class is 'undefined'`);
}

const classReflection = this.reflector.reflectClass(targetClass);
const classInstance: T = new targetClass();
const classInstance: TClass = new targetClass();

const props = classReflection.reduce((acc, property) => {
return { ...acc, [property.name]: this.handlePropertyValue(property) };
Expand Down
6 changes: 3 additions & 3 deletions packages/reflect/src/lib/class-reflector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Class } from '@mockinbird/types';
import { Type } from '@mockinbird/types';
import reflect, { ClassReflection, PropertyReflection } from '@plumier/reflect';
import { MockOptions } from '../types/mock-options.type';
import { MockOptions } from '../types';
import { MOCK_DECORATOR_NAME } from '../decorators/mock.decorator';
import { Property } from './property';
import { ClassReflectionDto } from '../types/class-reflection-dto.type';
Expand All @@ -23,7 +23,7 @@ export class ClassReflector {
});
}

public reflectClass(target: Class<unknown>): ClassReflectionDto {
public reflectClass(target: Type<unknown>): ClassReflectionDto {
if (!ClassReflector.REFLECTED_CLASSES.hasOwnProperty(target.name)) {
ClassReflector.REFLECTED_CLASSES[target.name] = this.extractDecoratedProperties(reflect(target));
}
Expand Down
6 changes: 3 additions & 3 deletions packages/reflect/src/types/mock-options.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Callback, Class, ClassLiteral, EnumObject, ExactValue, MultiClass } from '@mockinbird/types';
import { Callback, Type, ClassLiteral, EnumObject, ExactValue, MultiClass } from '@mockinbird/types';

export type MockOptions = Callback | ExactValue | Class | EnumObject | MultiClass;
export type MockOptions = Callback | ExactValue | Type | EnumObject | MultiClass;

export type GeneratedMock<TClass extends Class = any> = Class<TClass> | ClassLiteral<TClass>;
export type GeneratedMock<TClass extends Type = any> = Type<TClass> | ClassLiteral<TClass>;

0 comments on commit cf56561

Please sign in to comment.