Skip to content

Commit

Permalink
refactor: tests and general structure (#7)
Browse files Browse the repository at this point in the history
* refactor: naming

* refactor(FML): mega refactor

* refactor(handlers): logics and more handlers

* fix(primitives): throw an error when type mismatch

* fix(circularity): remove ciruclarity protection because reflect-metadata protection

* test(handlers): added unit test for callback handler

* test(handlers): add enum unit test and fix callback one

* test(handlers): add some more tests for handlers

* refactor(naming): changed some names

* test: more coverage and renaming

* chore(release-settings): change startegy to work with v-* and not with master

* test(reflector): add class reflector test coverage

* test: added integration test, increased coverage
  • Loading branch information
omermorad committed Jan 23, 2021
1 parent 7fa06de commit c7e1537
Show file tree
Hide file tree
Showing 46 changed files with 1,077 additions and 649 deletions.
8 changes: 6 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,16 @@ workflows:
- test
- build
filters:
tags:
only: /^v.*/
branches:
only: master
ignore: /.*/

- release:
requires:
- approve
filters:
tags:
only: /^v.*/
branches:
only: master
ignore: /.*/
2 changes: 1 addition & 1 deletion .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"branches": "master",
"branches": ["master", "next"],
"repositoryUrl": "https://github.com/omermorad/faker.ts.git",
"debug": "true",
"plugins": [
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"ts-node": "8.10.2",
"tsconfig-paths": "^3.9.0",
"tslint-config-airbnb": "^5.11.2",
"typescript": "^4.0.0"
"typescript": "^3.9.7"
},
"lint-staged": {
"*.ts": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
import { ClassProcessor } from './class-processor';
import * as faker from 'faker';
import { mocked } from 'ts-jest/utils';
import { Fixture } from '../decorators';
import * as reflectModule from '@plumier/reflect';
import { ClassReflection } from '@plumier/reflect';
import { ClassProcessor } from './class-processor';
import { mocked } from 'ts-jest/utils';
import { Fixture } from './decorators/fixture.decorator';
import { ClassReflector } from './class-reflector';
import FakerStatic = Faker.FakerStatic;

jest.mock('faker');

const factory = new ClassProcessor(faker, 'en');
const fakerMock = ({
setLocale: jest.fn(),
} as unknown) as FakerStatic;

const factory = new ClassProcessor(fakerMock, new ClassReflector(), 'en');
const reflectSpy = jest.spyOn(reflectModule, 'default');

class Dog {
@Fixture()
readonly name: string;

@Fixture()
readonly age: number;
}

const reflection: ClassReflection = {
kind: 'Class',
name: 'Dog',
decorators: [],
methods: [],
properties: [],
ctor: {
kind: 'Constructor',
name: 'constructor',
parameters: [],
},
typeClassification: 'Class',
super: Dog,
type: Dog,
};

describe('ClassProcessor', () => {
let reflectionMock;

class Dog {
@Fixture()
readonly name: string;

@Fixture()
readonly age: number;
}

const reflection: ClassReflection = {
kind: 'Class',
name: 'Dog',
decorators: [],
methods: [],
properties: [],
ctor: {
kind: 'Constructor',
name: 'constructor',
parameters: [],
},
typeClassification: 'Class',
super: Dog,
type: Dog,
};

beforeEach(() => {
reflectionMock = JSON.parse(JSON.stringify(reflection));
});
Expand All @@ -47,11 +49,13 @@ describe('ClassProcessor', () => {
});

test('faker.setLocale should be called with the passed locale', () => {
const setLocaleMock = mocked(faker.setLocale).mock.calls;
const setLocaleMock = mocked(fakerMock.setLocale).mock.calls;

expect(setLocaleMock).toHaveLength(1);
expect(setLocaleMock[0][0]).toEqual('en');
});

/*
test('Should throw an error if no target was passed', () => {
delete reflectionMock.properties;
reflectSpy.mockImplementationOnce(() => reflectionMock);
Expand All @@ -60,6 +64,7 @@ describe('ClassProcessor', () => {
expect(res).toEqual(undefined);
});
*/

test('Should return specific value passed from fixture', () => {
class AnotherDog {
Expand Down
60 changes: 60 additions & 0 deletions src/class-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ClassReflector } from './class-reflector';
import { CallbackValueHandler } from './handlers/callback-value-handler';
import { ObjectLiteralValueHandler } from './handlers/object-literal-value-handler';
import { EnumValueHandler } from './handlers/enum-value-handler';
import { MultiClassValueHandler } from './handlers/multi-class-value-handler';
import { SingleClassValueHandler } from './handlers/single-class-value-handler';
import { PrimitiveValueHandler } from './handlers/primitive-value-handler';
import { ClassLiteral, ClassType, FixtureOptions } from './types/fixture-options.type';
import { PropertyDto } from './types/property-dto.interface';
import { ValueHandler } from './types/value-handler.interface';
import { IClassProcessor } from './types/iclass-processor.interface';

import FakerStatic = Faker.FakerStatic;

export class ClassProcessor<T> implements IClassProcessor<T> {
private static readonly VALUE_INSPECTORS: ClassType<ValueHandler<FixtureOptions>>[] = [
EnumValueHandler,
MultiClassValueHandler,
SingleClassValueHandler,
CallbackValueHandler,
ObjectLiteralValueHandler,
PrimitiveValueHandler,
];

public static readonly DEFAULT_LOCALE = 'en';

public constructor(private readonly faker: FakerStatic, private readonly reflector: ClassReflector, locale: string) {
this.faker.setLocale(locale);
}

private handlePropertyValue(propertyDto: PropertyDto<FixtureOptions>): T | T[] {
for (const inspectorClass of ClassProcessor.VALUE_INSPECTORS) {
const inspector = new inspectorClass(this.faker, this) as ValueHandler<FixtureOptions>;

if (inspector.shouldHandle(propertyDto)) {
return inspector.produceValue<T>(propertyDto);
}
}

return null;
}

/**
* Return an object from the target class with all the properties
* decorated by the 'Fixture' Decorator
*
* @param target
*/
public process(target: ClassType<T>): ClassLiteral<T> {
if (!target) {
throw new Error(`Target class '${target}' is 'undefined'`);
}

const classReflection = this.reflector.reflectClass(target);

return classReflection.reduce((acc, val) => {
return { ...acc, [val.name]: this.handlePropertyValue(val) };
}, {});
}
}
52 changes: 52 additions & 0 deletions src/class-reflector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ClassReflector } from './class-reflector';
import { Fixture } from './decorators/fixture.decorator';
import { ClassReflectionDto } from './types/class-reflection-dto.type';

describe('ClassReflector', () => {
let reflector: ClassReflector;

class EmptyClass {}

class TestClass {
@Fixture('Foo')
fooer: string;

@Fixture('Bar')
barer: string;
}

describe('Given a ClassReflector', () => {
beforeAll(() => {
reflector = new ClassReflector();
});

describe("when calling 'reflectClass'", () => {
describe('and there are no related decorators on any of the properties', () => {
test('then return empty array of properties', () => {
expect(reflector.reflectClass(EmptyClass)).toHaveLength(0);
});
});

describe('and there are some related decorators in the class', () => {
let classReflection: ClassReflectionDto;

beforeAll(() => {
classReflection = reflector.reflectClass(TestClass);
});

test('then return an array of properties which the length is the number of decorators', () => {
expect(classReflection).toBeInstanceOf(Array);
expect(classReflection).toHaveLength(2);
});

test('then create a property dto for each of the properties', () => {
expect(Object.keys(classReflection[0])).toEqual(['type', 'value', 'name', 'constructorName']);
});

test('then register the class in the reflected classes storage', () => {
expect(ClassReflector.REFLECTED_CLASSES).toHaveProperty(TestClass.name);
});
});
});
});
});
46 changes: 46 additions & 0 deletions src/class-reflector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import reflect, { ClassReflection, PropertyReflection } from '@plumier/reflect';
import { ClassType } from './types/fixture-options.type';
import { FixtureOptions } from './types/fixture-options.type';
import { FIXTURE_DECORATOR_NAME } from './decorators/fixture.decorator';
import { PropertyDto } from './types/property-dto.interface';
import { ClassReflectionDto } from './types/class-reflection-dto.type';

export class ClassReflector {
public static readonly REFLECTED_CLASSES: Record<string, ClassReflectionDto> = {};

private extractDecoratedProperties(classReflection: ClassReflection): PropertyDto<FixtureOptions>[] {
return classReflection.properties?.map((property) => {
const value = ClassReflector.extractFixtureDecoratorValue(property);
return ClassReflector.createPropertyDto(property, value);
});
}

private static createPropertyDto(
property: PropertyReflection,
fixtureDecoratorValue: FixtureOptions | null
): PropertyDto<FixtureOptions> {
const { name, type: { name: constructorName } = {} } = property;

return {
type: typeof fixtureDecoratorValue,
value: fixtureDecoratorValue,
name,
constructorName,
};
}

private static extractFixtureDecoratorValue(property: PropertyReflection): FixtureOptions | null {
const { decorators = [] } = property;
const fixtureDecorator = decorators.find((decorator) => decorator.type === FIXTURE_DECORATOR_NAME);

return fixtureDecorator ? fixtureDecorator.value : null;
}

public reflectClass(target: ClassType<unknown>): ClassReflectionDto {
if (!ClassReflector.REFLECTED_CLASSES.hasOwnProperty(target.name)) {
ClassReflector.REFLECTED_CLASSES[target.name] = this.extractDecoratedProperties(reflect(target));
}

return ClassReflector.REFLECTED_CLASSES[target.name];
}
}
10 changes: 5 additions & 5 deletions src/decorators/fixture.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'reflect-metadata';
import { decorateProperty } from '@plumier/reflect';
import { ClassType } from '../types/class.type';
import { Callback, ClassType, EnumObject, ExactValue, MultiClass } from '../types/fixture-options.type';
import { FixtureOptions } from '../types/fixture-options.type';

export const FIXTURE_DECORATOR_NAME = 'Fixture';
Expand All @@ -22,7 +22,7 @@ export function Fixture(): PropertyDecorator;
* @param callback
* @constructor
*/
export function Fixture(callback: (faker: Faker.FakerStatic) => any): PropertyDecorator;
export function Fixture(callback: Callback): PropertyDecorator;

/**
* Generate the exact given value
Expand All @@ -35,7 +35,7 @@ export function Fixture(callback: (faker: Faker.FakerStatic) => any): PropertyDe
* @param value
* @constructor
*/
export function Fixture(value: string | number | boolean): PropertyDecorator;
export function Fixture(value: ExactValue): PropertyDecorator;

/**
* Generate an object matching to the given class (assuming the class is decorated with Fixture)
Expand All @@ -59,15 +59,15 @@ export function Fixture(value: ClassType): PropertyDecorator;
* @param options: { enum: object }
* @constructor
*/
export function Fixture(options: { enum: object }): PropertyDecorator;
export function Fixture(options: EnumObject): PropertyDecorator;

/**
* Generate multiple objects matching the given class (assuming the class is decorated with Fixture)
*
* @param options: { type: ClassType; count: number }
* @constructor
*/
export function Fixture(options: { type: ClassType; count: number }): PropertyDecorator;
export function Fixture(options: MultiClass): PropertyDecorator;

/**
* Fixture property decorator. This decorator will be parsed and will determine
Expand Down
Loading

0 comments on commit c7e1537

Please sign in to comment.