Skip to content

Commit

Permalink
support constructor verification
Browse files Browse the repository at this point in the history
  • Loading branch information
Roaders committed Oct 16, 2023
1 parent 9667227 commit f86b480
Show file tree
Hide file tree
Showing 7 changed files with 725 additions and 28 deletions.
3 changes: 3 additions & 0 deletions main/helper/lookup-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export function getLookup<T, C extends ConstructorFunction<T>, U extends LookupT
lookupType: U,
): FunctionCallLookup<T, C, U> {
switch (lookupType) {
case 'constructor':
return mock.constructorCallLookup as FunctionCallLookup<T, C, U>;
case 'function':
return mock.functionCallLookup as FunctionCallLookup<T, C, U>;
case 'getter':
Expand All @@ -18,5 +20,6 @@ export function getLookup<T, C extends ConstructorFunction<T>, U extends LookupT
case 'staticSetter':
return mock.staticSetterCallLookup as FunctionCallLookup<T, C, U>;
}

throw new Error(`Unknown lookup type: ${lookupType}`);
}
42 changes: 37 additions & 5 deletions main/mock/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,34 +38,55 @@ export type FunctionCallLookup<T, C extends ConstructorFunction<T>, U extends Lo
[P in FunctionName<T, C, U>]?: LookupParams<T, C, U, P>[];
};

export type LookupFunction<
T,
C extends ConstructorFunction<T>,
U extends LookupType,
K extends FunctionName<T, C, U>,
> = VerifierTarget<T, C, U>[K];

export type LookupParams<
T,
C extends ConstructorFunction<T>,
U extends LookupType,
K extends FunctionName<T, C, U>,
> = U extends FunctionTypes
? FunctionParams<VerifierTarget<T, C, U>[K]>
> = U extends 'constructor'
? ConstructorParams<C>
: U extends FunctionTypes
? FunctionParams<LookupFunction<T, C, U, K>>
: U extends SetterTypes
? [VerifierTarget<T, C, U>[K]]
? [LookupFunction<T, C, U, K>]
: [];

export type FunctionParams<T> = T extends (...args: infer P) => any ? P : never;

export type ConstructorFunction<T> = (abstract new (...args: any[]) => T) | (new (...args: any[]) => T);

export type ConstructorParams<T extends ConstructorFunction<any>> = T extends abstract new (
...args: infer RAbstract
) => T
? RAbstract
: T extends new (...args: infer R) => any
? R
: never;

export type StaticLookupTypes = 'staticFunction' | 'staticGetter' | 'staticSetter';
export type InstanceLookupTypes = 'function' | 'getter' | 'setter';
export type SetterTypes = 'staticSetter' | 'setter';
export type GetterTypes = 'staticGetter' | 'getter';
export type FunctionTypes = 'staticFunction' | 'function';
export type LookupType = StaticLookupTypes | InstanceLookupTypes;
export type LookupType = StaticLookupTypes | InstanceLookupTypes | 'constructor';

export type VerifierTarget<T, C extends ConstructorFunction<T>, U extends LookupType> = U extends StaticLookupTypes
export type VerifierTarget<T, C extends ConstructorFunction<T>, U extends LookupType> = U extends 'constructor'
? { constructor: C }
: U extends StaticLookupTypes
? U extends FunctionTypes
? FunctionsOnly<C>
: C
: U extends FunctionTypes
? FunctionsOnly<T>
: T;

export type FunctionName<T, C extends ConstructorFunction<T>, U extends LookupType> = keyof VerifierTarget<T, C, U>;

export interface IFunctionWithParametersVerification<
Expand Down Expand Up @@ -137,6 +158,7 @@ export interface IMocked<T, C extends ConstructorFunction<T> = never> {
*/
mockConstructor: C;

constructorCallLookup: FunctionCallLookup<T, C, 'constructor'>;
functionCallLookup: FunctionCallLookup<T, C, 'function'>;
setterCallLookup: FunctionCallLookup<T, C, 'setter'>;
getterCallLookup: FunctionCallLookup<T, C, 'getter'>;
Expand All @@ -160,6 +182,8 @@ export interface IMocked<T, C extends ConstructorFunction<T> = never> {
*/
setup(...operators: OperatorFunction<T, C>[]): IMocked<T, C>;

setupConstructor(): IFunctionWithParametersVerification<ConstructorParams<C>, T, 'constructor', C>;

/**
* Sets up a single function and returns a function verifier to verify calls made and parameters passed.
*
Expand Down Expand Up @@ -228,6 +252,14 @@ export interface IMocked<T, C extends ConstructorFunction<T> = never> {
setter?: (value: C[K]) => void,
): { getter: IFunctionVerifier<T, 'staticGetter', C>; setter: IFunctionVerifier<T, 'staticSetter', C> };

/**
* Verifies calls to constructor.
* expect(myMock.withFunction("functionName")).wasNotCalled():
* expect(myMock.withFunction("functionName")).wasCalledOnce():
* expect(myMock.withFunction("functionName").withParameters("one", 2)).wasCalledOnce():
*/
withConstructor(): IFunctionWithParametersVerification<ConstructorParams<C>, T, 'constructor', C>;

/**
* Verifies calls to a previously setup function.
* expect(myMock.withFunction("functionName")).wasNotCalled():
Expand Down
16 changes: 13 additions & 3 deletions main/mock/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@ import { addMatchers } from './matchers';
import {
defineProperty,
defineStaticProperty,
setupConstructor,
setupFunction,
setupProperty,
setupStaticFunction,
setupStaticProperty,
} from './operators';
import { createFunctionParameterVerifier, createFunctionVerifier } from './verifiers';
import {
createConstructorParameterVerifier,
createFunctionParameterVerifier,
createFunctionVerifier,
} from './verifiers';

export class Mock {
public static create<T, C extends ConstructorFunction<T> = never>(): IMocked<T, C> {
addMatchers();
const mocked: IMocked<T, C> = {
constructorCallLookup: {},
functionCallLookup: {},
setterCallLookup: {},
getterCallLookup: {},
Expand All @@ -26,15 +32,18 @@ export class Mock {

mock: {} as T,

// eslint-disable-next-line @typescript-eslint/no-empty-function
mockConstructor: ((..._args: any[]) => {}) as any,
mockConstructor: class MockConstructor {} as C,

setup: (...operators: OperatorFunction<T, C>[]) => {
let operatorMocked = mocked;
operators.forEach((operator) => (operatorMocked = operator(mocked)));
return operatorMocked;
},

setupConstructor: () => {
setupConstructor<T, C>()(mocked);
return mocked.withConstructor();
},
setupFunction: <K extends keyof FunctionsOnly<T>>(functionName: K, mockFunction?: any) => {
setupFunction<T, C, K>(functionName, mockFunction)(mocked);
return mocked.withFunction(functionName);
Expand Down Expand Up @@ -69,6 +78,7 @@ export class Mock {
return { getter: mocked.withStaticGetter(propertyName), setter: mocked.withStaticSetter(propertyName) };
},

withConstructor: () => createConstructorParameterVerifier(mocked),
withFunction: <U extends keyof FunctionsOnly<T>>(functionName: U) =>
createFunctionParameterVerifier(mocked, 'function', functionName),
withSetter: <U extends keyof T>(functionName: U) =>
Expand Down
35 changes: 35 additions & 0 deletions main/mock/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { getLookup } from '../helper';
import {
ConstructorFunction,
ConstructorParams,
FunctionCallLookup,
FunctionName,
FunctionTypes,
Expand All @@ -13,6 +14,27 @@ import {
SetterTypes,
} from './contracts';

/**
* Mocks a function on an existing Mock.
* Allows function call verification to be performed later in the test.
* You can optionally set a mock function implementation that will be called.
*
* @param functionName
* @param mockFunction
*/
export function setupConstructor<T, C extends ConstructorFunction<T>>(): OperatorFunction<T, C> {
return (mocked: IMocked<T, C>) => {
(mocked.mockConstructor = class MockConstructor {
constructor(...args: ConstructorParams<C>) {
trackConstructorCall(mocked, args as any);
}
} as C),
(mocked.constructorCallLookup['constructor'] = []);

return mocked;
};
}

/**
* Mocks a function on an existing Mock.
* Allows function call verification to be performed later in the test.
Expand Down Expand Up @@ -242,6 +264,15 @@ function definePropertyImpl<
return mocked;
}

export function trackConstructorCall<T, C extends ConstructorFunction<T>>(
mock: IMocked<T, C>,
params: LookupParams<T, C, 'constructor', 'constructor'>,
) {
const lookup = getLookup(mock, 'constructor');

trackCall(lookup, 'constructor', params);
}

function trackFunctionCall<
T,
C extends ConstructorFunction<T>,
Expand Down Expand Up @@ -288,5 +319,9 @@ function trackCall<T, C extends ConstructorFunction<T>, U extends LookupType, K
lookup[name] = functionCalls;
}

if (typeof functionCalls?.push != 'function') {
console.log(`WTF ${typeof functionCalls}`, { functionCalls });
}

functionCalls.push(params);
}
34 changes: 32 additions & 2 deletions main/mock/verifiers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getLookup, runningInJest } from '../helper';
import {
ConstructorFunction,
ConstructorParams,
FunctionName,
FunctionParams,
IFunctionVerifier,
Expand All @@ -10,12 +11,12 @@ import {
IMocked,
IParameterMatcher,
IStrictFunctionVerification,
LookupFunction,
LookupParams,
LookupType,
MatchFunction,
ParameterMatcher,
SetterTypes,
VerifierTarget,
} from './contracts';
import { isParameterMatcher, mapItemToString, toBe, toEqual } from './parameterMatchers';

Expand All @@ -24,7 +25,30 @@ export type VerifierParams<
C extends ConstructorFunction<T>,
U extends LookupType,
K extends FunctionName<T, C, U>,
> = U extends SetterTypes ? [VerifierTarget<T, C, U>[K]] : FunctionParams<VerifierTarget<T, C, U>[K]>;
> = U extends SetterTypes ? [LookupFunction<T, C, U, K>] : FunctionParams<LookupFunction<T, C, U, K>>;

export function createConstructorParameterVerifier<T, C extends ConstructorFunction<T>>(
mocked: IMocked<T, C>,
): IFunctionWithParametersVerification<ConstructorParams<C>, T, 'constructor', C> {
return {
...createFunctionVerifier(mocked, 'constructor', 'constructor'),
/**
* withParameters and withParametersEqualTo should have signatures:
*
* withParameters: (...parameters: FunctionParameterMatchers<VerifierParams<T, C, U, K>>)
* withParametersEqualTo: (...parameters: FunctionParameterMatchers<VerifierParams<T, C, U, K>>)
*
* but this gives the error: [ts] A rest parameter must be of an array type. [2370]
* https://github.com/microsoft/TypeScript/issues/29919
*
* so we internally type the function as any. This does not affect the extrnal facing function type
*/
withParameters: ((...parameters: ParameterMatcher<any>[]) =>
verifyParameters(parameters, mocked, 'constructor', 'constructor', false)) as any,
withParametersEqualTo: ((...parameters: ParameterMatcher<any>[]) =>
verifyParameters(parameters, mocked, 'constructor', 'constructor', true)) as any,
};
}

export function createFunctionParameterVerifier<
T,
Expand Down Expand Up @@ -146,6 +170,12 @@ export function verifyFunctionCalled<T, C extends ConstructorFunction<T>, U exte
const functionCalls: LookupParams<T, C, U, any>[] | undefined = getLookup(mock, type)[functionName];

switch (type) {
case 'constructor':
expectationMessage = `Expected constructor to be called`;
errorMessageSetupFunction = `Mock.setupConstructor()`;
errorMessageDescription = `Constructor`;
break;

case 'staticGetter':
expectationMessage = `Expected static property "${functionName}" getter to be called`;
errorMessageSetupFunction = `Mock.setupStaticProperty()`;
Expand Down
32 changes: 21 additions & 11 deletions package-lock.json

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

Loading

0 comments on commit f86b480

Please sign in to comment.