Skip to content

Commit

Permalink
feat(registerMock): add registerMock functionality to register custom…
Browse files Browse the repository at this point in the history
… mocks per project (#125)

* add first working version of register mock, needs code cleaning and unit tests for extension parameters

* remove unwanted extra functionalities

* make sure register mock works fine when not using caching

* add new extension strategy example and write documentation for registerMock

* fix after rebase

* fix playground

* remove commented code

* simplify code

* use typescript methods to check node type

* remove duplicated check for mocked type
  • Loading branch information
Pmyl authored and uittorio committed Dec 31, 2019
1 parent 990ecf1 commit 0feb05a
Show file tree
Hide file tree
Showing 27 changed files with 625 additions and 77 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,57 @@ mockList[0].id // id0
mockList[1].id // id1
```

#### Register mock
registerMock will register your custom mock that will be used in favour of creating a new one

./person.ts
```ts person.ts
export interface Person {
id: string;
}
```

./person-fake.ts
```ts person-fake.ts
import { Person } from './person';

export class PersonFake extends Person {
public id: string;
public name: string;

constructor() {
this.id = "Basic Id";
this.name = "Basic name";
}
}
```

./context.ts
```ts context.ts
import { registerMock } from 'ts-auto-mock';
import { Person } from './person';
import { PersonFake } from './person-fake';

registerMock<Person>(() => new PersonFake());
```

./my-test.ts
```ts my-test.ts
interface Wrapper {
person: Person;
}

const mock: Wrapper = createMock<Wrapper>();
mock.person // PersonFake
mock.person.id // "Basic Id"
mock.person.name // "Basic name"
```

When using a fake we recommend using the [extension strategy](docs/EXTENSION.md) to retrieve the fake object.
An example of usage for Promise->FakePromise can be found in [the test folder](test/registerMock/extensionStrategy/extensionStrategy.test.ts).

**Note:** You can use it only in the common file (webpack context.ts, mocha tsnode.js, etc), using `registerMock` in other files will have unexpected results.

## Type Examples
The library try to convert the type given to createMock so you dont need to create concrete mock manually.
[Open this link to see more examples](docs/DETAILS.md)
Expand Down
7 changes: 7 additions & 0 deletions config/karma/karma.config.registerMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const karmaBaseConfig = require('./karma.config.base');

module.exports = function(config) {
const karmaConfig = karmaBaseConfig(config, '../../test/registerMock/context.ts');

config.set(karmaConfig);
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
"build:transformer": "webpack --config config/modules/transformer/webpack.js",
"build:modules": "webpack --config config/modules/webpack.js",
"build": "npm run build:modules && npm run build:transformer",
"test": "npm run test:transformer && npm run test:framework:context && npm run test:framework && npm run test:frameworkDeprecated && npm run test:unit",
"test": "npm run test:transformer && npm run test:framework:context && npm run test:framework && npm run test:frameworkDeprecated && npm run test:registerMock && npm run test:unit",
"test:unit": "karma start config/karma/karma.config.unit.js",
"test:transformer": "karma start config/karma/karma.config.transformer.js",
"test:registerMock": "karma start config/karma/karma.config.registerMock.js",
"test:playground": "karma start config/karma/karma.config.transformer.playground.js",
"test:playground:build": "karma start config/karma/karma.config.transformer.playground.build.js",
"test:framework:context": "karma start config/karma/karma.config.framework.context.js",
Expand Down
21 changes: 19 additions & 2 deletions src/extension/extensionHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Extension } from './extension';
import { isFunction } from './method/function';

type AsMockedPropertyHandler<TMockedPropertyHandler, TMock, TPropName extends keyof TMock> = (prop: TMock[TPropName], mock: TMock, propName: TPropName) => TMockedPropertyHandler;

export class ExtensionHandler<TMock> {
private readonly _mock: TMock;
Expand All @@ -7,7 +10,21 @@ export class ExtensionHandler<TMock> {
this._mock = mock;
}

public get<TRequestedOverriddenMock>(extension: Extension<TMock, TRequestedOverriddenMock>): TRequestedOverriddenMock {
return extension(this._mock);
public get<TPropName extends keyof TMock, TMockedPropertyHandler>(
propertyName: TPropName,
asMockedPropertyHandler: AsMockedPropertyHandler<TMockedPropertyHandler, TMock, TPropName>,
): TMockedPropertyHandler;
public get<TMockedPropertyHandler>(
extension: Extension<TMock, TMockedPropertyHandler>,
): TMockedPropertyHandler;
public get<TPropName extends keyof TMock, TMockedPropertyHandler>(
extensionOrPropertyName: Function | TPropName,
maybePropertyHandler?: AsMockedPropertyHandler<TMockedPropertyHandler, TMock, TPropName>,
): TMockedPropertyHandler {
if (isFunction(extensionOrPropertyName)) {
return extensionOrPropertyName(this._mock);
}

return maybePropertyHandler(this._mock[extensionOrPropertyName], this._mock, extensionOrPropertyName);
}
}
4 changes: 4 additions & 0 deletions src/extension/method/function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// tslint:disable-next-line:no-any
export function isFunction(functionToCheck: any): functionToCheck is Function {
return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { createMock } from './create-mock';
export { createMockList } from './create-mock-list';
export { registerMock } from './register-mock';
1 change: 1 addition & 0 deletions src/register-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare function registerMock<T extends object>(factory: () => T): void;
2 changes: 1 addition & 1 deletion src/transformer/descriptor/intersection/intersection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function GetIntersectionDescriptor(intersectionTypeNode: ts.IntersectionT
const nodes: ts.Node[] = GetTypes(intersectionTypeNode.types, scope);

const hasInvalidIntersections: boolean = nodes.some((node: ts.Node) => {
return TypescriptHelper.IsLiteralOrPrimitive(node);
return TypescriptHelper.IsLiteralOrPrimitive(node) || ts.isTypeQueryNode(node);
});

if (hasInvalidIntersections) {
Expand Down
4 changes: 1 addition & 3 deletions src/transformer/descriptor/mock/mockCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { TypescriptCreator } from '../../helper/creator';
import { GetMockInternalValuesName, GetMockObjectReturnValueName } from './mockDeclarationName';
import { GetMockMarkerProperty, Property } from './mockMarker';

export function GetMockCall(
properties: ts.PropertyAssignment[],
signature: ts.Expression): ts.CallExpression {
export function GetMockCall(properties: ts.PropertyAssignment[], signature: ts.Expression): ts.CallExpression {
const mockObjectReturnValueName: ts.Identifier = GetMockObjectReturnValueName();
const mockInternalValuesName: ts.Identifier = GetMockInternalValuesName();

Expand Down
9 changes: 7 additions & 2 deletions src/transformer/descriptor/typeReference/typeReference.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as ts from 'typescript';
import { GetMockFactoryCall } from '../../mockFactoryCall/mockFactoryCall';
import { MockDefiner } from '../../mockDefiner/mockDefiner';
import { CreateMockFactory, GetMockFactoryCall } from '../../mockFactoryCall/mockFactoryCall';
import { Scope } from '../../scope/scope';
import { isTypeReferenceReusable } from '../../typeReferenceReusable/typeReferenceReusable';
import { GetDescriptor } from '../descriptor';
Expand All @@ -9,12 +10,16 @@ import { GetTypescriptType, IsTypescriptType } from '../tsLibs/typecriptLibs';
export function GetTypeReferenceDescriptor(node: ts.TypeReferenceNode, scope: Scope): ts.Expression {
const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(node.typeName);

if (MockDefiner.instance.hasMockForDeclaration(declaration)) {
return GetMockFactoryCall(node, scope);
}

if (IsTypescriptType(declaration)) {
return GetDescriptor(GetTypescriptType(node, scope), scope);
}

if (isTypeReferenceReusable(declaration)) {
return GetMockFactoryCall(node, scope);
return CreateMockFactory(node, scope);
}

return GetDescriptor(declaration, scope);
Expand Down
2 changes: 1 addition & 1 deletion src/transformer/helper/creator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ts from 'typescript';

export namespace TypescriptCreator {
export function createArrowFunction(block: ts.Block, parameter: ReadonlyArray<ts.ParameterDeclaration> = []): ts.ArrowFunction {
export function createArrowFunction(block: ts.ConciseBody, parameter: ReadonlyArray<ts.ParameterDeclaration> = []): ts.ArrowFunction {
return ts.createArrowFunction([], [], parameter, undefined, ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), block);
}

Expand Down
12 changes: 11 additions & 1 deletion src/transformer/matcher/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export function isCreateMockList(declaration: ts.FunctionDeclaration): boolean {
return declaration.name && declaration.name.getText() === 'createMockList';
}

export function isRegisterMock(declaration: ts.FunctionDeclaration): boolean {
return declaration.name && declaration.name.getText() === 'registerMock';
}

export function isFromTsAutoMock(signature: ts.Signature): boolean {
if (!isDeclarationDefined(signature)) {
return false;
Expand All @@ -21,6 +25,7 @@ export function isFromTsAutoMock(signature: ts.Signature): boolean {

const createMockTs: string = path.join(__dirname, `../create-mock.d.ts`);
const createMockListTs: string = path.join(__dirname, `../create-mock-list.d.ts`);
const registerMockTs: string = path.join(__dirname, `../register-mock.d.ts`);
const fileName: string = signature.declaration.getSourceFile().fileName;

const isCreateMockUrl: boolean = path.relative(fileName, createMockTs) === '';
Expand All @@ -33,7 +38,12 @@ export function isFromTsAutoMock(signature: ts.Signature): boolean {
TransformerLogger().unexpectedCreateMock(fileName, createMockListTs);
}

return isCreateMockUrl || isCreateMockListUrl;
const isRegisterMockUrl: boolean = path.relative(fileName, registerMockTs) === '';
if (fileName.indexOf('register-mock.d.ts') > -1 && !isRegisterMockUrl) {
TransformerLogger().unexpectedCreateMock(fileName, registerMockTs);
}

return isCreateMockUrl || isCreateMockListUrl || isRegisterMockUrl;
}

function isDeclarationDefined(signature: ts.Signature): boolean {
Expand Down
17 changes: 16 additions & 1 deletion src/transformer/mock/mock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as ts from 'typescript';
import { Logger } from '../../logger/logger';
import { ArrayHelper } from '../array/array';
import { GetDescriptor } from '../descriptor/descriptor';
import { TypescriptHelper } from '../descriptor/helper/helper';
import { TypescriptCreator } from '../helper/creator';
import { getMockMergeExpression, getMockMergeIteratorExpression } from '../mergeExpression/mergeExpression';
import { MockDefiner } from '../mockDefiner/mockDefiner';
import { Scope } from '../scope/scope';

function getMockExpression(nodeToMock: ts.TypeNode): ts.Expression {
Expand Down Expand Up @@ -48,7 +52,7 @@ export function getMockForList(nodeToMock: ts.TypeNode, node: ts.CallExpression)
const lengthLiteral: ts.NumericLiteral = node.arguments[0] as ts.NumericLiteral;

if (!lengthLiteral) {
return ts.createArrayLiteral([]);
return ts.createArrayLiteral([]);
}

const length: number = getNumberFromNumericLiteral(lengthLiteral);
Expand All @@ -63,3 +67,14 @@ export function getMockForList(nodeToMock: ts.TypeNode, node: ts.CallExpression)

return ts.createArrayLiteral(mockList);
}

export function storeRegisterMock(typeToMock: ts.TypeNode, node: ts.CallExpression): ts.Node {
if (ts.isTypeReferenceNode(typeToMock)) {
const factory: ts.FunctionExpression = node.arguments[0] as ts.FunctionExpression;
MockDefiner.instance.storeRegisterMockFor(TypescriptHelper.GetDeclarationFromNode(typeToMock.typeName), factory);
} else {
Logger('RegisterMock').error('registerMock can be used only to mock type references.');
}

return ts.createEmptyStatement();
}
83 changes: 52 additions & 31 deletions src/transformer/mockDefiner/mockDefiner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ export class MockDefiner {
private _neededImportIdentifierPerFile: { [key: string]: Array<ModuleNameIdentifier> } = {};
private _internalModuleImportIdentifierPerFile: { [key: string]: { [key in ModuleName]: ts.Identifier } } = {};
private _factoryRegistrationsPerFile: FactoryRegistrationPerFile = {};
private _registerMockFactoryRegistrationsPerFile: FactoryRegistrationPerFile = {};
private _factoryIntersectionsRegistrationsPerFile: FactoryIntersectionRegistrationPerFile = {};
private _factoryCache: DeclarationCache;
private _registerMockFactoryCache: DeclarationCache;
private _declarationCache: DeclarationCache;
private _factoryIntersectionCache: DeclarationListCache;
private _fileName: string;
Expand All @@ -45,6 +47,7 @@ export class MockDefiner {
this._declarationCache = new DeclarationCache();
this._factoryIntersectionCache = new DeclarationListCache();
this._factoryUniqueName = new FactoryUniqueName();
this._registerMockFactoryCache = new DeclarationCache();
this._cacheEnabled = GetTsAutoMockCacheOptions();
}

Expand Down Expand Up @@ -90,6 +93,7 @@ export class MockDefiner {
...this._getImportsToAddInFile(sourceFile),
...this._getExportsToAddInFile(sourceFile),
...this._getExportsIntersectionToAddInFile(sourceFile),
...this._getRegisterMockInFile(sourceFile),
];
}

Expand All @@ -101,12 +105,28 @@ export class MockDefiner {
}
this._factoryRegistrationsPerFile[sourceFile.fileName] = [];
this._factoryIntersectionsRegistrationsPerFile[sourceFile.fileName] = [];
this._registerMockFactoryRegistrationsPerFile[sourceFile.fileName] = [];
}

public getMockFactory(declaration: ts.Declaration): ts.Expression {
const key: string = this._getMockFactoryId(declaration);
public createMockFactory(declaration: ts.Declaration): void {
const thisFileName: string = this._fileName;

return this.getMockFactoryByKey(key);
const key: string = this.getDeclarationKeyMap(declaration);

this._factoryCache.set(declaration, key);

this._factoryRegistrationsPerFile[thisFileName] = this._factoryRegistrationsPerFile[thisFileName] || [];

const descriptor: ts.Expression = GetDescriptor(declaration, new Scope(key));

const mockGenericParameter: ts.ParameterDeclaration = this._getMockGenericParameter();

const factory: ts.FunctionExpression = TypescriptCreator.createFunctionExpressionReturn(descriptor, [mockGenericParameter]);

this._factoryRegistrationsPerFile[thisFileName].push({
key: declaration,
factory,
});
}

public getMockFactoryTypeofEnum(declaration: ts.EnumDeclaration): ts.Expression {
Expand Down Expand Up @@ -137,6 +157,22 @@ export class MockDefiner {
return this._declarationCache.get(declaration);
}

public storeRegisterMockFor(declaration: ts.Declaration, factory: ts.FunctionExpression): void {
const key: string = this.getDeclarationKeyMap(declaration);

this._registerMockFactoryCache.set(declaration, key);

this._registerMockFactoryRegistrationsPerFile[this._fileName] = this._registerMockFactoryRegistrationsPerFile[this._fileName] || [];
this._registerMockFactoryRegistrationsPerFile[this._fileName].push({
key: declaration,
factory,
});
}

public hasMockForDeclaration(declaration: ts.Declaration): boolean {
return this._factoryCache.has(declaration) || this._registerMockFactoryCache.has(declaration);
}

private _mockRepositoryAccess(filename: string): ts.Expression {
const repository: ts.Identifier = this._getModuleIdentifier(filename, ModuleName.Repository);

Expand All @@ -152,34 +188,6 @@ export class MockDefiner {
private _getModuleIdentifier(fileName: string, module: ModuleName): ts.Identifier {
return this._internalModuleImportIdentifierPerFile[fileName][module];
}

private _getMockFactoryId(declaration: ts.Declaration): string {
const thisFileName: string = this._fileName;

if (this._factoryCache.has(declaration)) {
return this._factoryCache.get(declaration);
}

const key: string = this._declarationCache.get(declaration);

this._factoryCache.set(declaration, key);

this._factoryRegistrationsPerFile[thisFileName] = this._factoryRegistrationsPerFile[thisFileName] || [];

const descriptor: ts.Expression = GetDescriptor(declaration, new Scope(key));

const mockGenericParameter: ts.ParameterDeclaration = this._getMockGenericParameter();

const factory: ts.FunctionExpression = TypescriptCreator.createFunctionExpressionReturn(descriptor, [mockGenericParameter]);

this._factoryRegistrationsPerFile[thisFileName].push({
key: declaration,
factory,
});

return key;
}

private _getMockFactoryIdForTypeofEnum(declaration: ts.EnumDeclaration): string {
const thisFileName: string = this._fileName;

Expand Down Expand Up @@ -266,6 +274,19 @@ export class MockDefiner {
return [];
}

private _getRegisterMockInFile(sourceFile: ts.SourceFile): ts.Statement[] {
if (this._registerMockFactoryRegistrationsPerFile[sourceFile.fileName]) {
return this._registerMockFactoryRegistrationsPerFile[sourceFile.fileName]
.map((reg: { key: ts.Declaration; factory: ts.Expression }) => {
const key: string = this._registerMockFactoryCache.get(reg.key);

return this._createRegistration(sourceFile.fileName, key, reg.factory);
});
}

return [];
}

private _createRegistration(fileName: string, key: string, factory: ts.Expression): ts.Statement {
return ts.createExpressionStatement(
ts.createCall(
Expand Down

0 comments on commit 0feb05a

Please sign in to comment.