From 944b9876c5549d167cf8b1600608731950649079 Mon Sep 17 00:00:00 2001 From: Pmyl Date: Tue, 31 Dec 2019 07:46:46 +0000 Subject: [PATCH] feat(registerMock): add registerMock functionality to register custom 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 --- README.md | 51 ++++++++++ config/karma/karma.config.registerMock.js | 7 ++ package.json | 3 +- src/extension/extensionHandler.ts | 21 +++- src/extension/method/function.ts | 4 + src/index.ts | 1 + src/register-mock.ts | 1 + .../descriptor/intersection/intersection.ts | 2 +- src/transformer/descriptor/mock/mockCall.ts | 4 +- .../descriptor/typeReference/typeReference.ts | 9 +- src/transformer/helper/creator.ts | 2 +- src/transformer/matcher/matcher.ts | 12 ++- src/transformer/mock/mock.ts | 17 +++- src/transformer/mockDefiner/mockDefiner.ts | 83 ++++++++++------ .../mockFactoryCall/mockFactoryCall.ts | 62 +++++++----- src/transformer/transformer.ts | 8 +- test/playground/playground.test.ts | 16 +-- test/registerMock/context.ts | 9 ++ .../extensionStrategy.test.ts | 25 +++++ test/registerMock/fakePromise.ts | 34 +++++++ test/registerMock/interface/interface.test.ts | 18 ++++ .../mockingPromise/mockingPromise.test.ts | 98 +++++++++++++++++++ test/registerMock/typeAlias/typeAlias.test.ts | 32 ++++++ .../typeLiteral/typeLiteral.test.ts | 29 ++++++ test/registerMock/typeQuery/typeQuery.test.ts | 88 +++++++++++++++++ .../descriptor/typeQuery/typeQuery.test.ts | 48 +++++++++ test/transformer/register-mock.test.ts | 18 ++++ 27 files changed, 625 insertions(+), 77 deletions(-) create mode 100644 config/karma/karma.config.registerMock.js create mode 100644 src/extension/method/function.ts create mode 100644 src/register-mock.ts create mode 100644 test/registerMock/context.ts create mode 100644 test/registerMock/extensionStrategy/extensionStrategy.test.ts create mode 100644 test/registerMock/fakePromise.ts create mode 100644 test/registerMock/interface/interface.test.ts create mode 100644 test/registerMock/mockingPromise/mockingPromise.test.ts create mode 100644 test/registerMock/typeAlias/typeAlias.test.ts create mode 100644 test/registerMock/typeLiteral/typeLiteral.test.ts create mode 100644 test/registerMock/typeQuery/typeQuery.test.ts create mode 100644 test/transformer/register-mock.test.ts diff --git a/README.md b/README.md index 390bddbc8..a69ec1d2a 100644 --- a/README.md +++ b/README.md @@ -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(() => new PersonFake()); +``` + +./my-test.ts +```ts my-test.ts +interface Wrapper { + person: Person; +} + +const mock: Wrapper = createMock(); +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) diff --git a/config/karma/karma.config.registerMock.js b/config/karma/karma.config.registerMock.js new file mode 100644 index 000000000..5d1f00e02 --- /dev/null +++ b/config/karma/karma.config.registerMock.js @@ -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); +}; diff --git a/package.json b/package.json index 29c7ad3cc..640e91c2a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/extension/extensionHandler.ts b/src/extension/extensionHandler.ts index 3b1c6b6ef..8f353fcbf 100644 --- a/src/extension/extensionHandler.ts +++ b/src/extension/extensionHandler.ts @@ -1,4 +1,7 @@ import { Extension } from './extension'; +import { isFunction } from './method/function'; + +type AsMockedPropertyHandler = (prop: TMock[TPropName], mock: TMock, propName: TPropName) => TMockedPropertyHandler; export class ExtensionHandler { private readonly _mock: TMock; @@ -7,7 +10,21 @@ export class ExtensionHandler { this._mock = mock; } - public get(extension: Extension): TRequestedOverriddenMock { - return extension(this._mock); + public get( + propertyName: TPropName, + asMockedPropertyHandler: AsMockedPropertyHandler, + ): TMockedPropertyHandler; + public get( + extension: Extension, + ): TMockedPropertyHandler; + public get( + extensionOrPropertyName: Function | TPropName, + maybePropertyHandler?: AsMockedPropertyHandler, + ): TMockedPropertyHandler { + if (isFunction(extensionOrPropertyName)) { + return extensionOrPropertyName(this._mock); + } + + return maybePropertyHandler(this._mock[extensionOrPropertyName], this._mock, extensionOrPropertyName); } } diff --git a/src/extension/method/function.ts b/src/extension/method/function.ts new file mode 100644 index 000000000..b3518399b --- /dev/null +++ b/src/extension/method/function.ts @@ -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]'; +} diff --git a/src/index.ts b/src/index.ts index 5df56e6e3..68a88e0f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export { createMock } from './create-mock'; export { createMockList } from './create-mock-list'; +export { registerMock } from './register-mock'; diff --git a/src/register-mock.ts b/src/register-mock.ts new file mode 100644 index 000000000..61ffa953d --- /dev/null +++ b/src/register-mock.ts @@ -0,0 +1 @@ +export declare function registerMock(factory: () => T): void; diff --git a/src/transformer/descriptor/intersection/intersection.ts b/src/transformer/descriptor/intersection/intersection.ts index 030486b2b..8b73cfb7a 100644 --- a/src/transformer/descriptor/intersection/intersection.ts +++ b/src/transformer/descriptor/intersection/intersection.ts @@ -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) { diff --git a/src/transformer/descriptor/mock/mockCall.ts b/src/transformer/descriptor/mock/mockCall.ts index d4f80cd6f..02b721675 100644 --- a/src/transformer/descriptor/mock/mockCall.ts +++ b/src/transformer/descriptor/mock/mockCall.ts @@ -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(); diff --git a/src/transformer/descriptor/typeReference/typeReference.ts b/src/transformer/descriptor/typeReference/typeReference.ts index 1aa1259f8..39c7a433d 100644 --- a/src/transformer/descriptor/typeReference/typeReference.ts +++ b/src/transformer/descriptor/typeReference/typeReference.ts @@ -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'; @@ -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); diff --git a/src/transformer/helper/creator.ts b/src/transformer/helper/creator.ts index 4af570d57..1ca0fb9d2 100644 --- a/src/transformer/helper/creator.ts +++ b/src/transformer/helper/creator.ts @@ -1,7 +1,7 @@ import * as ts from 'typescript'; export namespace TypescriptCreator { - export function createArrowFunction(block: ts.Block, parameter: ReadonlyArray = []): ts.ArrowFunction { + export function createArrowFunction(block: ts.ConciseBody, parameter: ReadonlyArray = []): ts.ArrowFunction { return ts.createArrowFunction([], [], parameter, undefined, ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), block); } diff --git a/src/transformer/matcher/matcher.ts b/src/transformer/matcher/matcher.ts index 8d2991853..900bbf000 100644 --- a/src/transformer/matcher/matcher.ts +++ b/src/transformer/matcher/matcher.ts @@ -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; @@ -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) === ''; @@ -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 { diff --git a/src/transformer/mock/mock.ts b/src/transformer/mock/mock.ts index 6cec1d46a..342be224b 100644 --- a/src/transformer/mock/mock.ts +++ b/src/transformer/mock/mock.ts @@ -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 { @@ -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); @@ -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(); +} diff --git a/src/transformer/mockDefiner/mockDefiner.ts b/src/transformer/mockDefiner/mockDefiner.ts index c3d2eba1c..b3b0c6f3e 100644 --- a/src/transformer/mockDefiner/mockDefiner.ts +++ b/src/transformer/mockDefiner/mockDefiner.ts @@ -32,8 +32,10 @@ export class MockDefiner { private _neededImportIdentifierPerFile: { [key: string]: Array } = {}; 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; @@ -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(); } @@ -90,6 +93,7 @@ export class MockDefiner { ...this._getImportsToAddInFile(sourceFile), ...this._getExportsToAddInFile(sourceFile), ...this._getExportsIntersectionToAddInFile(sourceFile), + ...this._getRegisterMockInFile(sourceFile), ]; } @@ -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 { @@ -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); @@ -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; @@ -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( diff --git a/src/transformer/mockFactoryCall/mockFactoryCall.ts b/src/transformer/mockFactoryCall/mockFactoryCall.ts index e01ceeb3d..6be1a385c 100644 --- a/src/transformer/mockFactoryCall/mockFactoryCall.ts +++ b/src/transformer/mockFactoryCall/mockFactoryCall.ts @@ -8,24 +8,16 @@ import { MockGenericParameter } from '../mockGeneric/mockGenericParameter'; import { Scope } from '../scope/scope'; export function GetMockFactoryCall(typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.Expression { - const genericDeclaration: IGenericDeclaration = GenericDeclaration(scope); const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(typeReferenceNode.typeName); - const declarationKey: string = MockDefiner.instance.getDeclarationKeyMap(declaration); - - if (typeReferenceNode.typeArguments) { - genericDeclaration.addFromTypeReferenceNode(typeReferenceNode, declarationKey); - } - addFromDeclarationExtensions(declaration as GenericDeclarationSupported, declarationKey, genericDeclaration); + return getDeclarationMockFactoryCall(declaration, typeReferenceNode, scope); +} - const genericsParametersExpression: ts.ObjectLiteralExpression[] = genericDeclaration.getExpressionForAllGenerics(); - const mockFactoryCall: ts.Expression = MockDefiner.instance.getMockFactory(declaration); +export function CreateMockFactory(typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.Expression { + const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(typeReferenceNode.typeName); + MockDefiner.instance.createMockFactory(declaration); - return ts.createCall( - mockFactoryCall, - [], - [ts.createArrayLiteral(genericsParametersExpression)], - ); + return getDeclarationMockFactoryCall(declaration, typeReferenceNode, scope); } export function GetMockFactoryCallIntersection(intersection: ts.IntersectionTypeNode, scope: Scope): ts.Expression { @@ -51,9 +43,9 @@ export function GetMockFactoryCallIntersection(intersection: ts.IntersectionType const mockFactoryCall: ts.Expression = MockDefiner.instance.getMockFactoryIntersection(declarations, intersection); return ts.createCall( - mockFactoryCall, - [], - [ts.createArrayLiteral(genericsParametersExpression)], + mockFactoryCall, + [], + [ts.createArrayLiteral(genericsParametersExpression)], ); } @@ -61,19 +53,39 @@ export function GetMockFactoryCallTypeofEnum(declaration: ts.EnumDeclaration): t const mockFactoryCall: ts.Expression = MockDefiner.instance.getMockFactoryTypeofEnum(declaration); return ts.createCall( - mockFactoryCall, - [], - [], + mockFactoryCall, + [], + [], ); } export function GetMockFactoryCallForThis(mockKey: string): ts.Expression { const mockFactoryCall: ts.Expression = MockDefiner.instance.getMockFactoryByKey(mockKey); + return ts.createCall( + mockFactoryCall, + [], + [MockGenericParameter], + ); +} + +function getDeclarationMockFactoryCall(declaration: ts.Declaration, typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.Expression { + const declarationKey: string = MockDefiner.instance.getDeclarationKeyMap(declaration); + const mockFactoryCall: ts.Expression = MockDefiner.instance.getMockFactoryByKey(declarationKey); + const genericDeclaration: IGenericDeclaration = GenericDeclaration(scope); + + if (typeReferenceNode.typeArguments) { + genericDeclaration.addFromTypeReferenceNode(typeReferenceNode, declarationKey); + } + + addFromDeclarationExtensions(declaration as GenericDeclarationSupported, declarationKey, genericDeclaration); + + const genericsParametersExpression: ts.ObjectLiteralExpression[] = genericDeclaration.getExpressionForAllGenerics(); + return ts.createCall( mockFactoryCall, [], - [MockGenericParameter], + [ts.createArrayLiteral(genericsParametersExpression)], ); } @@ -87,10 +99,10 @@ function addFromDeclarationExtensions(declaration: GenericDeclarationSupported, const extensionDeclarationKey: string = MockDefiner.instance.getDeclarationKeyMap(extensionDeclaration); genericDeclaration.addFromDeclarationExtension( - declarationKey, - extensionDeclaration as GenericDeclarationSupported, - extensionDeclarationKey, - extension); + declarationKey, + extensionDeclaration as GenericDeclarationSupported, + extensionDeclarationKey, + extension); addFromDeclarationExtensions(extensionDeclaration as GenericDeclarationSupported, extensionDeclarationKey, genericDeclaration); } diff --git a/src/transformer/transformer.ts b/src/transformer/transformer.ts index 1663b80ed..72101767d 100644 --- a/src/transformer/transformer.ts +++ b/src/transformer/transformer.ts @@ -1,7 +1,7 @@ import * as ts from 'typescript'; import { SetTsAutoMockOptions, TsAutoMockOptions } from '../options/options'; -import { isCreateMock, isCreateMockList, isFromTsAutoMock } from './matcher/matcher'; -import { getMock, getMockForList } from './mock/mock'; +import { isCreateMock, isCreateMockList, isFromTsAutoMock, isRegisterMock } from './matcher/matcher'; +import { getMock, getMockForList, storeRegisterMock } from './mock/mock'; import { MockDefiner } from './mockDefiner/mockDefiner'; import { SetTypeChecker, TypeChecker } from './typeChecker/typeChecker'; @@ -54,6 +54,10 @@ function visitNode(node: ts.Node): ts.Node { return getMockForList(nodeToMock, node); } + if (isRegisterMock(declaration)) { + return storeRegisterMock(nodeToMock, node); + } + return node; } diff --git a/test/playground/playground.test.ts b/test/playground/playground.test.ts index 81bd98793..904bf5c61 100644 --- a/test/playground/playground.test.ts +++ b/test/playground/playground.test.ts @@ -1,4 +1,4 @@ -import { createMock } from 'ts-auto-mock'; +import { createMock, registerMock } from 'ts-auto-mock'; import { MyEnum } from './enums'; /* @@ -10,11 +10,13 @@ import { MyEnum } from './enums'; */ it('should work', () => { - interface A { + interface A { + a: string; + } + const enumm2: typeof MyEnum = createMock(); + expect(createMock().a).toBe("ok"); - } - const enumm2: typeof MyEnum = createMock(); - createMock(); - - expect(enumm2.A).toEqual(0); + expect(enumm2.A).toEqual(0); + + expect(createMock().a).toBe("ok"); }); diff --git a/test/registerMock/context.ts b/test/registerMock/context.ts new file mode 100644 index 000000000..cff883262 --- /dev/null +++ b/test/registerMock/context.ts @@ -0,0 +1,9 @@ +import { registerMock } from 'ts-auto-mock'; +import { FakePromise } from './fakePromise'; + +registerMock>(() => new FakePromise()); + +// @ts-ignore +// tslint:disable-next-line:typedef +const registerMockContext = require.context('./', true, /\.test\.ts$/); +registerMockContext.keys().map(registerMockContext); diff --git a/test/registerMock/extensionStrategy/extensionStrategy.test.ts b/test/registerMock/extensionStrategy/extensionStrategy.test.ts new file mode 100644 index 000000000..bc006226e --- /dev/null +++ b/test/registerMock/extensionStrategy/extensionStrategy.test.ts @@ -0,0 +1,25 @@ +import { createMock } from 'ts-auto-mock'; +import { On } from 'ts-auto-mock/extension'; +import { FakePromise } from '../fakePromise'; + +function asFakePromise, TFake extends FakePromise>(prop: Promise): FakePromise { + return prop as unknown as FakePromise; +} + +describe('extension strategy for fake promise', () => { + it('should retrieve the fake promise correctly', (done: Function) => { + interface WithPromise { + promise: Promise; + a: number; + } + + const mock: WithPromise = createMock(); + + On(mock).get('promise', asFakePromise).resolve('custom resolution value'); + + mock.promise.then((value: string) => { + expect(value).toBe('custom resolution value'); + done(); + }); + }); +}); diff --git a/test/registerMock/fakePromise.ts b/test/registerMock/fakePromise.ts new file mode 100644 index 000000000..671c23222 --- /dev/null +++ b/test/registerMock/fakePromise.ts @@ -0,0 +1,34 @@ +export class FakePromise { + public readonly [Symbol.toStringTag]: string; + private _promise: Promise; + private _res: (t: T) => void; + private _rej: (e: any) => void; + + constructor() { + const that: FakePromise = this; + this._promise = new Promise(function (res, rej) { + that._res = res; + that._rej = rej; + }); + } + + public resolve(t: T): void { + this._res(t); + } + + public reject(t: any): void { + this._rej(t); + } + + public then(t: (tt: T) => (T2 | Promise)): Promise { + return this._promise.then(t); + } + + public catch(t: any): Promise { + return this._promise.catch(t); + } + + public finally(): Promise { + return this._promise.finally(); + } +} diff --git a/test/registerMock/interface/interface.test.ts b/test/registerMock/interface/interface.test.ts new file mode 100644 index 000000000..41b951635 --- /dev/null +++ b/test/registerMock/interface/interface.test.ts @@ -0,0 +1,18 @@ +import { createMock, registerMock } from 'ts-auto-mock'; + +describe('registerMock for interface', () => { + it('should override standard behaviour of mock creation', () => { + interface APropInterface { + internalProp: string; + } + + interface AParentInterface { + prop: APropInterface; + } + + registerMock(() => ({ internalProp: 'whaaat' })); + const mock: AParentInterface = createMock(); + + expect(mock.prop.internalProp).toBe('whaaat'); + }); +}); diff --git a/test/registerMock/mockingPromise/mockingPromise.test.ts b/test/registerMock/mockingPromise/mockingPromise.test.ts new file mode 100644 index 000000000..32705edd4 --- /dev/null +++ b/test/registerMock/mockingPromise/mockingPromise.test.ts @@ -0,0 +1,98 @@ +import { createMock } from 'ts-auto-mock'; +import { FakePromise } from '../fakePromise'; + +describe('mocking the registered promise', () => { + describe('when doing it in intersection', () => { + it('should not interfere', () => { + type A = {} & Promise; + const intersectionMock: A = createMock(); + const actualPromiseMock: Promise = createMock>(); + + expect(intersectionMock.then).toBeUndefined(); + expect(actualPromiseMock.then).not.toBeUndefined(); + expect(actualPromiseMock.constructor).toBe(FakePromise); + }); + }); + + describe('when doing it in intersection with generic', () => { + it('should not interfere', () => { + type A = {} & Promise; + + const intersectionMock: A = createMock>(); + const actualPromiseMock: Promise = createMock>(); + + expect(intersectionMock.then).toBeUndefined(); + expect(actualPromiseMock.then).not.toBeUndefined(); + expect(actualPromiseMock.constructor).toBe(FakePromise); + }); + }); + + describe('when doing it in type alias', () => { + it('should use the registered mock for simple assignment', () => { + type A = Promise; + + const typeAliasMock: A = createMock>(); + const actualPromiseMock: Promise = createMock>(); + + expect(typeAliasMock.then).not.toBeUndefined(); + expect(typeAliasMock.constructor).toBe(FakePromise); + expect(actualPromiseMock.then).not.toBeUndefined(); + expect(actualPromiseMock.constructor).toBe(FakePromise); + }); + + it('should use the registered mock for deep assignment', () => { + type A = Promise; + type B = A; + + const typeAliasMock: B = createMock>(); + const actualPromiseMock: Promise = createMock>(); + + expect(typeAliasMock.then).not.toBeUndefined(); + expect(typeAliasMock.constructor).toBe(FakePromise); + expect(actualPromiseMock.then).not.toBeUndefined(); + expect(actualPromiseMock.constructor).toBe(FakePromise); + }); + }); + + describe('when doing it in an interface', () => { + it('should not interfere for an extension', () => { + interface A extends Promise { + + } + + const interfaceMock: A = createMock(); + const actualPromiseMock: Promise = createMock>(); + + expect(interfaceMock.then).toBeUndefined(); + expect(actualPromiseMock.then).not.toBeUndefined(); + expect(actualPromiseMock.constructor).toBe(FakePromise); + }); + + it('should not interfere for an extension with generics', () => { + interface A extends Promise { + + } + + const interfaceMock: A = createMock>(); + const actualPromiseMock: Promise = createMock>(); + + expect(interfaceMock.then).toBeUndefined(); + expect(actualPromiseMock.then).not.toBeUndefined(); + expect(actualPromiseMock.constructor).toBe(FakePromise); + }); + + it('should use the registered mock in a property', () => { + interface A { + prop: Promise; + } + + const interfaceMock: A = createMock>(); + const actualPromiseMock: Promise = createMock>(); + + expect(interfaceMock.prop.then).not.toBeUndefined(); + expect(interfaceMock.prop.constructor).toBe(FakePromise); + expect(actualPromiseMock.then).not.toBeUndefined(); + expect(actualPromiseMock.constructor).toBe(FakePromise); + }); + }); +}); diff --git a/test/registerMock/typeAlias/typeAlias.test.ts b/test/registerMock/typeAlias/typeAlias.test.ts new file mode 100644 index 000000000..2e3656f90 --- /dev/null +++ b/test/registerMock/typeAlias/typeAlias.test.ts @@ -0,0 +1,32 @@ +import { createMock, registerMock } from 'ts-auto-mock'; + +describe('registerMock for type alias', () => { + it('should override standard behaviour of mock creation', () => { + type APropType = { + internalProp: string; + } + + interface AParentInterface { + prop: APropType; + } + + registerMock(() => ({ internalProp: 'whaaat' })); + const mock: AParentInterface = createMock(); + + expect(mock.prop.internalProp).toBe('whaaat'); + }); + + it('should override standard behaviour of mock creation for intersection', () => { + type APropType = { internalProp: string; } & { else: number; } + + interface AParentInterface { + prop: APropType; + } + + registerMock(() => ({ internalProp: 'whaaat', else: 53 })); + const mock: AParentInterface = createMock(); + + expect(mock.prop.internalProp).toBe('whaaat'); + expect(mock.prop.else).toBe(53); + }); +}); diff --git a/test/registerMock/typeLiteral/typeLiteral.test.ts b/test/registerMock/typeLiteral/typeLiteral.test.ts new file mode 100644 index 000000000..3d926b706 --- /dev/null +++ b/test/registerMock/typeLiteral/typeLiteral.test.ts @@ -0,0 +1,29 @@ +import { createMock, createMockList, registerMock } from 'ts-auto-mock'; + +describe('registerMock type literal', () => { + it('should never work', () => { + registerMock<{prop: string;}>(() => ({prop: 'mocked one'})); + + const mock1: {prop: string} = createMock<{prop: string;}>(); + const mock2: {prop: string}[] = createMockList<{prop: string;}>(1); + const mock3: { sub: {prop: string}; } = createMock<{ sub: {prop: string}; }>(); + + interface Interface { + sub: {prop: string}; + } + const mock4: Interface = createMock(); + + type Literal = {prop: string;}; + const mock5: Literal = createMock(); + + type Intersection = {prop: string;} & {some: number;}; + const mock6: Intersection = createMock(); + + expect(mock1.prop).toBe(''); + expect(mock2[0].prop).toBe(''); + expect(mock3.sub.prop).toBe(''); + expect(mock4.sub.prop).toBe(''); + expect(mock5.prop).toBe(''); + expect(mock6.prop).toBe(''); + }); +}); diff --git a/test/registerMock/typeQuery/typeQuery.test.ts b/test/registerMock/typeQuery/typeQuery.test.ts new file mode 100644 index 000000000..4030a450f --- /dev/null +++ b/test/registerMock/typeQuery/typeQuery.test.ts @@ -0,0 +1,88 @@ +import { createMock, registerMock } from 'ts-auto-mock'; +import { ImportNamespace } from '../../transformer/descriptor/utils/interfaces/importNameSpace'; +import Interface2 = ImportNamespace.Interface2; + +describe('registerMock of typeQuery', () => { + it('should not work for enum', () => { + enum MyEnum { + A, + B = "B" + } + + registerMock(() => ({A: 0, B: MyEnum.B, C: "Something"})); + + const mock1: typeof MyEnum = createMock(); + const mock2: { sub: typeof MyEnum; } = createMock<{ sub: typeof MyEnum; }>(); + + interface Interface { + sub: typeof MyEnum; + } + const mock3: Interface = createMock(); + + type Literal = typeof MyEnum; + const mock4: Literal = createMock(); + + type Intersection = typeof MyEnum & {some: number;}; + const mock5: Intersection = createMock(); + + expect((mock1 as any).C).toBeUndefined(); + expect((mock2.sub as any).C).toBeUndefined(); + expect((mock3.sub as any).C).toBeUndefined(); + expect((mock4 as any).C).toBeUndefined(); + expect(mock5).toBeUndefined(); + }); + + it('should not work for class', () => { + class MyClass { + prop: string; + } + + registerMock(() => (Object.assign(class Some { prop: string; }, { C: 'something' }))); + + const mock1: typeof MyClass = createMock(); + const mock2: { sub: typeof MyClass; } = createMock<{ sub: typeof MyClass; }>(); + + interface Interface { + sub: typeof MyClass; + } + const mock3: Interface = createMock(); + + type Literal = typeof MyClass; + const mock4: Literal = createMock(); + + type Intersection = typeof MyClass & {some: number;}; + const mock5: Intersection = createMock(); + + expect((mock1 as any).C).toBeUndefined(); + expect((mock2.sub as any).C).toBeUndefined(); + expect((mock3.sub as any).C).toBeUndefined(); + expect((mock4 as any).C).toBeUndefined(); + expect(mock5).toBeUndefined(); + }); + + it('should not work for variable', () => { + const a: Interface2 = { b: 23 }; + + registerMock(() => ({ b: 45, C: 'something' })); + + const mock1: typeof a = createMock(); + const mock2: { sub: typeof a; } = createMock<{ sub: typeof a; }>(); + + interface Interface { + sub: typeof a; + } + const mock3: Interface = createMock(); + + type Literal = typeof a; + const mock4: Literal = createMock(); + + type Intersection = typeof a & {some: number;}; + const mock5: Intersection = createMock(); + + expect((mock1 as any).C).toBeUndefined(); + expect((mock2.sub as any).C).toBeUndefined(); + expect((mock3.sub as any).C).toBeUndefined(); + expect((mock4 as any).C).toBeUndefined(); + expect(mock5).toBeUndefined(); + }); +}); diff --git a/test/transformer/descriptor/typeQuery/typeQuery.test.ts b/test/transformer/descriptor/typeQuery/typeQuery.test.ts index c02ddd600..02066ffdd 100644 --- a/test/transformer/descriptor/typeQuery/typeQuery.test.ts +++ b/test/transformer/descriptor/typeQuery/typeQuery.test.ts @@ -39,9 +39,31 @@ describe('typeQuery', () => { expect(functionMock()).toEqual(0); }); + + it('should return undefined for an intersection', () => { + function func(): string { + return 'ok'; + } + + type Intersection = {} & typeof func; + + const functionMock: Intersection = createMock(); + + expect(functionMock).toBeUndefined(); + }); }); describe('for class', () => { + it('should create a newable class for a class declaration in file', () => { + class MyClass { + prop: string; + } + + const classMock: typeof MyClass = createMock(); + + expect(new classMock().prop).toEqual(''); + }); + it('should create a newable class for an imported class declaration', () => { const classMock: typeof ExportedDeclaredClass = createMock(); @@ -59,6 +81,14 @@ describe('typeQuery', () => { expect(new classMock().prop).toEqual(0); }); + + it('should return undefined for an intersection', () => { + type Intersection = {} & WrapExportedClass; + + const functionMock: Intersection = createMock(); + + expect(functionMock).toBeUndefined(); + }); }); describe('for enum', () => { @@ -99,6 +129,14 @@ describe('typeQuery', () => { expect(enumMock.B).toEqual('B'); expect(enumMock.C).toEqual('MaybeC'); }); + + it('should return undefined for an intersection', () => { + type Intersection = {} & WrapExportedEnum; + + const functionMock: Intersection = createMock(); + + expect(functionMock).toBeUndefined(); + }); }); describe('for variable', () => { @@ -118,6 +156,16 @@ describe('typeQuery', () => { expect(mock.A).toEqual(0); }); + it('should return undefined for an intersection', () => { + let aVariable: WrapExportedEnum; + + type Intersection = {} & typeof aVariable; + + const functionMock: Intersection = createMock(); + + expect(functionMock).toBeUndefined(); + }); + describe('inferred type', () => { it('should work for inferred object', () => { const aVariable = { prop: 'asd' }; diff --git a/test/transformer/register-mock.test.ts b/test/transformer/register-mock.test.ts new file mode 100644 index 000000000..5736a66e1 --- /dev/null +++ b/test/transformer/register-mock.test.ts @@ -0,0 +1,18 @@ +import { createMock, registerMock } from 'ts-auto-mock'; + +describe('register-mock', () => { + it('should override standard behaviour of mock creation', () => { + interface APropInterface { + internalProp: string; + } + + interface AParentInterface { + prop: APropInterface; + } + + registerMock(() => ({ internalProp: 'whaaat' })); + const mock: AParentInterface = createMock(); + + expect(mock.prop.internalProp).toBe('whaaat'); + }); +});