diff --git a/.eslintrc.js b/.eslintrc.js index 96b2dc5..e8f0ac7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,7 +20,7 @@ module.exports = { plugins: ['@typescript-eslint'], ignorePatterns: ['dist/', 'reports/', 'docs/'], rules: { - 'prettier/prettier': ['error', prettierConfig], + 'prettier/prettier': ['error', { trailingComma: 'all', tabWidth: 4, singleQuote: true, printWidth: 120 }], '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12f2722..a71af45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,5 +24,3 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build-release - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 diff --git a/main/helper/lookup-helper.ts b/main/helper/lookup-helper.ts index 9f6b980..4288473 100644 --- a/main/helper/lookup-helper.ts +++ b/main/helper/lookup-helper.ts @@ -5,6 +5,8 @@ export function getLookup, U extends LookupT lookupType: U, ): FunctionCallLookup { switch (lookupType) { + case 'constructorFunction': + return mock.constructorCallLookup as FunctionCallLookup; case 'function': return mock.functionCallLookup as FunctionCallLookup; case 'getter': @@ -18,5 +20,6 @@ export function getLookup, U extends LookupT case 'staticSetter': return mock.staticSetterCallLookup as FunctionCallLookup; } + throw new Error(`Unknown lookup type: ${lookupType}`); } diff --git a/main/mock/contracts.ts b/main/mock/contracts.ts index 284003c..76a6110 100644 --- a/main/mock/contracts.ts +++ b/main/mock/contracts.ts @@ -38,34 +38,55 @@ export type FunctionCallLookup, U extends Lo [P in FunctionName]?: LookupParams[]; }; +export type LookupFunction< + T, + C extends ConstructorFunction, + U extends LookupType, + K extends FunctionName, +> = VerifierTarget[K]; + export type LookupParams< T, C extends ConstructorFunction, U extends LookupType, K extends FunctionName, -> = U extends FunctionTypes - ? FunctionParams[K]> +> = U extends 'constructorFunction' + ? ConstructorParams + : U extends FunctionTypes + ? FunctionParams> : U extends SetterTypes - ? [VerifierTarget[K]] + ? [LookupFunction] : []; + export type FunctionParams = T extends (...args: infer P) => any ? P : never; export type ConstructorFunction = (abstract new (...args: any[]) => T) | (new (...args: any[]) => T); +export type ConstructorParams> = 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 | 'constructorFunction'; -export type VerifierTarget, U extends LookupType> = U extends StaticLookupTypes +export type VerifierTarget, U extends LookupType> = U extends 'constructorFunction' + ? { constructorFunction: C } + : U extends StaticLookupTypes ? U extends FunctionTypes ? FunctionsOnly : C : U extends FunctionTypes ? FunctionsOnly : T; + export type FunctionName, U extends LookupType> = keyof VerifierTarget; export interface IFunctionWithParametersVerification< @@ -137,6 +158,7 @@ export interface IMocked = never> { */ mockConstructor: C; + constructorCallLookup: FunctionCallLookup; functionCallLookup: FunctionCallLookup; setterCallLookup: FunctionCallLookup; getterCallLookup: FunctionCallLookup; @@ -160,6 +182,8 @@ export interface IMocked = never> { */ setup(...operators: OperatorFunction[]): IMocked; + setupConstructor(): IFunctionWithParametersVerification, T, 'constructorFunction', C>; + /** * Sets up a single function and returns a function verifier to verify calls made and parameters passed. * @@ -176,7 +200,10 @@ export interface IMocked = never> { * @param propertyname * @param value */ - setupProperty(propertyname: K, value?: T[K]): IFunctionVerifier; + setupProperty( + propertyname: K, + value?: T[K], + ): { getter: IFunctionVerifier; setter: IFunctionVerifier }; /** * Defines a single property and allows getters and setters to be defined. * Returns a function verifier to verify get and set operations @@ -189,7 +216,7 @@ export interface IMocked = never> { propertyname: K, getter?: () => T[K], setter?: (value: T[K]) => void, - ): IFunctionVerifier; + ): { getter: IFunctionVerifier; setter: IFunctionVerifier }; /** * Sets up a single static function and returns a function verifier to verify calls made and parameters passed. @@ -207,7 +234,10 @@ export interface IMocked = never> { * @param propertyname * @param value */ - setupStaticProperty(propertyname: K, value?: C[K]): IFunctionVerifier; + setupStaticProperty( + propertyname: K, + value?: C[K], + ): { getter: IFunctionVerifier; setter: IFunctionVerifier }; /** * Defines a single static property and allows getters and setters to be defined. * Returns a function verifier to verify get and set operations @@ -220,7 +250,15 @@ export interface IMocked = never> { propertyname: K, getter?: () => C[K], setter?: (value: C[K]) => void, - ): IFunctionVerifier; + ): { getter: IFunctionVerifier; setter: IFunctionVerifier }; + + /** + * Verifies calls to constructor. + * expect(myMock.withFunction("functionName")).wasNotCalled(): + * expect(myMock.withFunction("functionName")).wasCalledOnce(): + * expect(myMock.withFunction("functionName").withParameters("one", 2)).wasCalledOnce(): + */ + withConstructor(): IFunctionWithParametersVerification, T, 'constructorFunction', C>; /** * Verifies calls to a previously setup function. diff --git a/main/mock/mock.ts b/main/mock/mock.ts index 23b427d..f706059 100644 --- a/main/mock/mock.ts +++ b/main/mock/mock.ts @@ -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 = never>(): IMocked { addMatchers(); const mocked: IMocked = { + constructorCallLookup: {}, functionCallLookup: {}, setterCallLookup: {}, getterCallLookup: {}, @@ -26,8 +32,7 @@ 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[]) => { let operatorMocked = mocked; @@ -35,13 +40,17 @@ export class Mock { return operatorMocked; }, + setupConstructor: () => { + setupConstructor()(mocked); + return mocked.withConstructor(); + }, setupFunction: >(functionName: K, mockFunction?: any) => { setupFunction(functionName, mockFunction)(mocked); return mocked.withFunction(functionName); }, setupProperty: (propertyName: K, value?: T[K]) => { setupProperty(propertyName, value)(mocked); - return mocked.withGetter(propertyName); + return { getter: mocked.withGetter(propertyName), setter: mocked.withSetter(propertyName) }; }, defineProperty: ( propertyName: K, @@ -49,7 +58,7 @@ export class Mock { setter?: (value: T[K]) => void, ) => { defineProperty(propertyName, getter, setter)(mocked); - return mocked.withGetter(propertyName); + return { getter: mocked.withGetter(propertyName), setter: mocked.withSetter(propertyName) }; }, setupStaticFunction: >(functionName: K, mockFunction?: any) => { @@ -58,7 +67,7 @@ export class Mock { }, setupStaticProperty: (propertyName: K, value?: C[K]) => { setupStaticProperty(propertyName, value)(mocked); - return mocked.withStaticGetter(propertyName); + return { getter: mocked.withStaticGetter(propertyName), setter: mocked.withStaticSetter(propertyName) }; }, defineStaticProperty: ( propertyName: K, @@ -66,9 +75,10 @@ export class Mock { setter?: (value: C[K]) => void, ) => { defineStaticProperty(propertyName, getter, setter)(mocked); - return mocked.withStaticGetter(propertyName); + return { getter: mocked.withStaticGetter(propertyName), setter: mocked.withStaticSetter(propertyName) }; }, + withConstructor: () => createConstructorParameterVerifier(mocked), withFunction: >(functionName: U) => createFunctionParameterVerifier(mocked, 'function', functionName), withSetter: (functionName: U) => diff --git a/main/mock/operators.ts b/main/mock/operators.ts index 6446e5c..a5ae3a1 100644 --- a/main/mock/operators.ts +++ b/main/mock/operators.ts @@ -2,6 +2,7 @@ import { getLookup } from '../helper'; import { ConstructorFunction, + ConstructorParams, FunctionCallLookup, FunctionName, FunctionTypes, @@ -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>(): OperatorFunction { + return (mocked: IMocked) => { + mocked.mockConstructor = class MockConstructor { + constructor(...args: ConstructorParams) { + trackConstructorCall(mocked, args as any); + } + } as C; + mocked.constructorCallLookup['constructorFunction'] = []; + + return mocked; + }; +} + /** * Mocks a function on an existing Mock. * Allows function call verification to be performed later in the test. @@ -242,6 +264,15 @@ function definePropertyImpl< return mocked; } +export function trackConstructorCall>( + mock: IMocked, + params: LookupParams, +) { + const lookup = getLookup(mock, 'constructorFunction'); + + trackCall(lookup, 'constructorFunction', params); +} + function trackFunctionCall< T, C extends ConstructorFunction, diff --git a/main/mock/verifiers.ts b/main/mock/verifiers.ts index f4ca33b..7948988 100644 --- a/main/mock/verifiers.ts +++ b/main/mock/verifiers.ts @@ -1,6 +1,7 @@ import { getLookup, runningInJest } from '../helper'; import { ConstructorFunction, + ConstructorParams, FunctionName, FunctionParams, IFunctionVerifier, @@ -10,12 +11,12 @@ import { IMocked, IParameterMatcher, IStrictFunctionVerification, + LookupFunction, LookupParams, LookupType, MatchFunction, ParameterMatcher, SetterTypes, - VerifierTarget, } from './contracts'; import { isParameterMatcher, mapItemToString, toBe, toEqual } from './parameterMatchers'; @@ -24,7 +25,30 @@ export type VerifierParams< C extends ConstructorFunction, U extends LookupType, K extends FunctionName, -> = U extends SetterTypes ? [VerifierTarget[K]] : FunctionParams[K]>; +> = U extends SetterTypes ? [LookupFunction] : FunctionParams>; + +export function createConstructorParameterVerifier>( + mocked: IMocked, +): IFunctionWithParametersVerification, T, 'constructorFunction', C> { + return { + ...createFunctionVerifier(mocked, 'constructorFunction', 'constructorFunction'), + /** + * withParameters and withParametersEqualTo should have signatures: + * + * withParameters: (...parameters: FunctionParameterMatchers>) + * withParametersEqualTo: (...parameters: FunctionParameterMatchers>) + * + * 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[]) => + verifyParameters(parameters, mocked, 'constructorFunction', 'constructorFunction', false)) as any, + withParametersEqualTo: ((...parameters: ParameterMatcher[]) => + verifyParameters(parameters, mocked, 'constructorFunction', 'constructorFunction', true)) as any, + }; +} export function createFunctionParameterVerifier< T, @@ -143,9 +167,16 @@ export function verifyFunctionCalled, U exte let errorMessageSetupFunction: string; let errorMessageDescription: string; - const functionCalls: LookupParams[] | undefined = getLookup(mock, type)[functionName]; + const functionCallsLookup = getLookup(mock, type); + const functionCalls: LookupParams[] | undefined = functionCallsLookup[functionName]; switch (type) { + case 'constructorFunction': + 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()`; @@ -183,9 +214,12 @@ export function verifyFunctionCalled, U exte } if (functionCalls === undefined) { - return createCustomMatcherFailResult( - `${errorMessageDescription} "${functionName}" has not been setup. Please setup using ${errorMessageSetupFunction} before verifying calls.`, - ); + const message = + type === 'constructorFunction' + ? `Constructor has not been setup. Please setup using ${errorMessageSetupFunction} before verifying calls.` + : `${errorMessageDescription} "${functionName}" has not been setup. Please setup using ${errorMessageSetupFunction} before verifying calls.`; + + return createCustomMatcherFailResult(message); } const parameterMatchResults = functionCalls.map((params) => matchParameters(params, parameterMatchers)); diff --git a/main/tsconfig.json b/main/tsconfig.json index 313f88d..7db6270 100644 --- a/main/tsconfig.json +++ b/main/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../tsconfig.json", "exclude": [ - "dist" + "../dist" ], "files": [ "index.ts" diff --git a/package-lock.json b/package-lock.json index 356d07d..cb2ff3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@morgan-stanley/ts-mocking-bird", - "version": "0.7.0", + "version": "1.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@morgan-stanley/ts-mocking-bird", - "version": "0.7.0", + "version": "1.1.3", "license": "Apache-2.0", "dependencies": { "lodash": "^4.17.16", @@ -2730,14 +2730,24 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", + "version": "1.0.30001547", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", + "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] }, "node_modules/chalk": { "version": "2.4.2", @@ -4532,9 +4542,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { @@ -8462,6 +8472,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8916,9 +8932,9 @@ "dev": true }, "node_modules/socket.io-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", - "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -9512,23 +9528,24 @@ } }, "node_modules/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { "node": ">=6" } }, "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "engines": { "node": ">= 4.0.0" @@ -9950,6 +9967,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12494,9 +12521,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", + "version": "1.0.30001547", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", + "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", "dev": true }, "chalk": { @@ -13881,9 +13908,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true }, "form-data": { @@ -16803,6 +16830,12 @@ "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", "dev": true }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -17138,9 +17171,9 @@ "dev": true }, "socket.io-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", - "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, "requires": { "@socket.io/component-emitter": "~3.1.0", @@ -17589,20 +17622,21 @@ "dev": true }, "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "dependencies": { "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true } } @@ -17894,6 +17928,16 @@ "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 5d0a94a..e9cc243 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@morgan-stanley/ts-mocking-bird", - "version": "0.7.0", + "version": "1.1.3", "description": "A fully type safe mocking, call verification and import replacement library for jasmine and jest", "license": "Apache-2.0", "author": "Morgan Stanley", @@ -13,15 +13,14 @@ "copy": "copyfiles main/**/*.html main/**/*.css main/**/*.jpg dist", "prebuild-release": "npm run clean && npm run verify-release", "verify-release": "npm run build && concurrently --kill-others-on-fail npm:lint npm:test npm:test:jest", - "build-release": "npm run copy-dist && typedoc main/index.ts", - "watch-build": "tsc --watch", + "build-release": "typedoc main/index.ts", + "watch-build": "tsc --watch -p main/tsconfig.json", "test": "karma start --singleRun --browsers ChromeHeadlessNoSandbox", "test:karma": "karma start --singleRun --browsers ChromeHeadlessNoSandbox", "test:jest": "jest", "lint": "eslint . --ext .ts,.js", "watch-test": "karma start --no-coverage", "watch-test:jest": "jest --watch", - "copy-dist": "concurrently --kill-others-on-fail npm:copy-dist-*", "watch-test-coverage": "karma start", "lint:fix": "eslint . --ext .ts,.js --fix", "compatibility-test": "npx tsc spec/ts-compat-test.ts --noEmit --skipLibCheck --watch" diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index 004e315..0000000 --- a/prettier.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - trailingComma: 'all', - tabWidth: 4, - singleQuote: true, - printWidth: 120, -}; diff --git a/readme.md b/readme.md index fd2d8e0..91429e7 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,6 @@ ![Lifecycle Active](https://badgen.net/badge/Lifecycle/Active/green) ![npm](https://img.shields.io/npm/v/@morgan-stanley/ts-mocking-bird) [![Build Status](https://github.com/morganstanley/ts-mocking-bird/actions/workflows/build.yml/badge.svg)](https://github.com/Roaders/ts-mocking-bird/actions/workflows/build.yml) -[![codecov](https://codecov.io/gh/morganstanley/ts-mocking-bird/branch/master/graph/badge.svg)](https://codecov.io/gh/morganstanley/ts-mocking-bird) [![Known Vulnerabilities](https://snyk.io/test/github/morganstanley/ts-mocking-bird/badge.svg)](https://snyk.io/test/github/morganstanley/ts-mocking-bird}) ![NPM](https://img.shields.io/npm/l/@morgan-stanley/ts-mocking-bird) ![NPM](https://img.shields.io/badge/types-TypeScript-blue) @@ -117,6 +116,19 @@ expect( ).wasCalledOnce(); ``` +### Verify Constructor Parameters + +```ts +const mockInstance = Mock.create().setup( + setupConstructor(), + ); + +new mockInstance.mockConstructor(serviceInstance); + +expect(mockInstance.withConstructor().withParameters(serviceInstance)).wasCalledOnce(); + +``` + ### Verify Getter: ```typescript const mockedService: IMocked = Mock.create().setup(setupProperty('propOne')); diff --git a/spec/examples/exampleTests.spec.ts b/spec/examples/exampleTests.spec.ts index 9a197e9..dbd71f1 100644 --- a/spec/examples/exampleTests.spec.ts +++ b/spec/examples/exampleTests.spec.ts @@ -6,6 +6,7 @@ import { setupFunction, setupProperty, setupStaticFunction, + setupConstructor, } from '../../main'; import { ClassWithConstructorArgument, ClassWithInstanceArgument } from './exampleImplementation'; import { IMyService, MyService } from './exampleImports'; @@ -118,3 +119,16 @@ describe('verify function calls', () => { expect(functionVerifier.withParameters('someValue')).wasCalledOnce(); }); }); + +describe('verify constructor calls', () => { + it(`should verify that constructor was called`, () => { + const mockInstance = Mock.create().setup( + setupConstructor(), + ); + const mockService = Mock.create(); + + new mockInstance.mockConstructor(mockService.mock); + + expect(mockInstance.withConstructor().withParameters(mockService.mock)).wasCalledOnce(); + }); +}); diff --git a/spec/mock/mock-static.spec.ts b/spec/mock/mock-static.spec.ts index 6a94a28..156a324 100644 --- a/spec/mock/mock-static.spec.ts +++ b/spec/mock/mock-static.spec.ts @@ -700,20 +700,40 @@ describe('mock with statics', () => { }); describe('defineStaticProperty', () => { - it('called directly on mock instance', () => { - mocked.setup(defineStaticProperty('propertyOne')); + describe('getter', () => { + it('called directly on mock instance', () => { + mocked.setup(defineStaticProperty('propertyOne')); - get(mocked.mockConstructor.propertyOne); + get(mocked.mockConstructor.propertyOne); - expect(mocked.withStaticGetter('propertyOne')).wasCalledAtLeastOnce(); + expect(mocked.withStaticGetter('propertyOne')).wasCalledAtLeastOnce(); + }); + + it('called on checker returned from setup function', () => { + const verifier = mocked.defineStaticProperty('propertyOne').getter; + + get(mocked.mockConstructor.propertyOne); + + expect(verifier).wasCalledAtLeastOnce(); + }); }); - it('called on checker returned from setup function', () => { - const verifier = mocked.defineStaticProperty('propertyOne'); + describe('setter', () => { + it('called directly on mock instance', () => { + mocked.setup(defineStaticProperty('propertyOne')); - get(mocked.mockConstructor.propertyOne); + mocked.mockConstructor.propertyOne = 'one'; - expect(verifier).wasCalledAtLeastOnce(); + expect(mocked.withStaticSetter('propertyOne')).wasCalledAtLeastOnce(); + }); + + it('called on checker returned from setup function', () => { + const verifier = mocked.defineStaticProperty('propertyOne').setter; + + mocked.mockConstructor.propertyOne = 'one'; + + expect(verifier).wasCalledAtLeastOnce(); + }); }); }); @@ -727,7 +747,7 @@ describe('mock with statics', () => { }); it('called on checker returned from setup function', () => { - const verifier = mocked.setupStaticProperty('propertyOne'); + const verifier = mocked.setupStaticProperty('propertyOne').getter; get(mocked.mockConstructor.propertyOne); diff --git a/spec/mock/mock.spec.ts b/spec/mock/mock.spec.ts index a2d4a39..e4fd98b 100644 --- a/spec/mock/mock.spec.ts +++ b/spec/mock/mock.spec.ts @@ -15,7 +15,7 @@ import { toBeDefined, toEqual, } from '../../main'; -import { defineProperty, setupFunction, setupProperty } from '../../main/mock/operators'; +import { defineProperty, setupConstructor, setupFunction, setupProperty } from '../../main/mock/operators'; import { verifyFailure, verifyJestFailure } from './failure-verifier'; describe('mock', () => { @@ -217,6 +217,14 @@ describe('mock', () => { `Property "propertyTwo" has not been setup. Please setup using Mock.setupProperty() before verifying calls.`, ); }); + + it('withConstructor will fail with a meaningful error if we try to assert a function that is not setup', () => { + verifyFailure( + mocked.withConstructor(), + matchers.wasNotCalled(), + `Constructor has not been setup. Please setup using Mock.setupConstructor() before verifying calls.`, + ); + }); }); describe('withFunction', () => { @@ -857,12 +865,12 @@ describe('mock', () => { }); it('should count function calls the same regardless of called via mock or constructor', () => { - const constructedInstace = new mocked.mockConstructor({}, new Date()); + const constructedInstance = new mocked.mockConstructor(); - constructedInstace.functionWithParamsAndReturn('one', 123, true); + constructedInstance.functionWithParamsAndReturn('one', 123, true); mock.functionWithParamsAndReturn('two', 123, true); - constructedInstace.functionWithParamsAndReturn('one', 456, true); + constructedInstance.functionWithParamsAndReturn('one', 456, true); mock.functionWithParamsAndReturn('one', 456, false); verifyFailure( @@ -876,6 +884,583 @@ describe('mock', () => { }); }); + describe('withConstructor', () => { + it('called directly on mock instance', () => { + mocked.setup(setupConstructor()); + + new mocked.mockConstructor(); + + expect(mocked.withConstructor()).wasCalledAtLeastOnce(); + }); + + it('called on checker returned from setup function', () => { + const verifier = mocked.setupConstructor(); + + new mocked.mockConstructor(); + + expect(verifier).wasCalledAtLeastOnce(); + }); + + describe('assertion with no parameters', () => { + beforeEach(() => { + mocked.setup(setupConstructor()); + }); + afterEach(() => { + delete (expect as any).extend; + }); + + describe('wasCalledAtLeastOnce()', () => { + it('should not fail when function has been called once', () => { + new mocked.mockConstructor(); + + expect(mocked.withConstructor()).wasCalledAtLeastOnce(); + }); + + it('should not fail when function has been called multiple times', () => { + new mocked.mockConstructor(); + new mocked.mockConstructor(); + new mocked.mockConstructor(); + + expect(mocked.withConstructor()).wasCalledAtLeastOnce(); + }); + + it('should fail when function has not been called', () => { + verifyFailure( + mocked.withConstructor(), + matchers.wasCalledAtLeastOnce(), + `Expected constructor to be called but it was not.`, + ); + }); + + it('when running in jest it should return a CustomMatcherResult with a function for message', () => { + (expect as any).extend = () => console.log(`Temp function to fool we're using jest`); + + verifyJestFailure( + mocked.withConstructor(), + matchers.wasCalledAtLeastOnce(), + `Expected constructor to be called but it was not.`, + ); + }); + }); + + describe('wasNotCalled()', () => { + it('should fail when function has been called once', () => { + new mocked.mockConstructor(); + + verifyFailure( + mocked.withConstructor(), + matchers.wasNotCalled(), + `Expected constructor to be called 0 times but it was called 1 times with matching parameters and 1 times in total.`, + ); + }); + + it('should fail when function has been called multiple times', () => { + new mocked.mockConstructor(); + new mocked.mockConstructor(); + new mocked.mockConstructor(); + + verifyFailure( + mocked.withConstructor(), + matchers.wasNotCalled(), + `Expected constructor to be called 0 times but it was called 3 times with matching parameters and 3 times in total.`, + ); + }); + + it('should not fail when function has not been called', () => { + expect(mocked.withConstructor()).wasNotCalled(); + }); + }); + + describe('wasCalled(0)', () => { + it('should fail when function has been called once', () => { + new mocked.mockConstructor(); + + verifyFailure( + mocked.withConstructor(), + matchers.wasCalled(), + `Expected constructor to be called 0 times but it was called 1 times with matching parameters and 1 times in total.`, + 0, + ); + }); + + it("should throw an error if 'times: number' not passed to wasCalled", () => { + expect(() => (expect(mocked.withConstructor()) as any).wasCalled()).toThrowError( + 'Expected call count must be passed to wasCalled(times: number). To verify that it was called at least once use wasCalledAtLeastOnce().', + ); + }); + + it('should fail when function has been called multiple times', () => { + new mocked.mockConstructor(); + new mocked.mockConstructor(); + new mocked.mockConstructor(); + + verifyFailure( + mocked.withConstructor(), + matchers.wasCalled(), + `Expected constructor to be called 0 times but it was called 3 times with matching parameters and 3 times in total.`, + 0, + ); + }); + + it('should not fail when function has not been called', () => { + expect(mocked.withConstructor()).wasCalled(0); + }); + }); + + describe('wasCalledOnce', () => { + it('should not fail when function has been called once', () => { + new mocked.mockConstructor(); + + expect(mocked.withConstructor()).wasCalledOnce(); + }); + + it('should fail when function has been called multiple times', () => { + new mocked.mockConstructor(); + new mocked.mockConstructor(); + new mocked.mockConstructor(); + + verifyFailure( + mocked.withConstructor(), + matchers.wasCalledOnce(), + `Expected constructor to be called 1 times but it was called 3 times with matching parameters and 3 times in total.`, + ); + }); + + it('should fail when function has not been called', () => { + verifyFailure( + mocked.withConstructor(), + matchers.wasCalledOnce(), + `Expected constructor to be called 1 times but it was called 0 times with matching parameters and 0 times in total.`, + ); + }); + }); + + describe('wasCalled(1)', () => { + it('should not fail when function has been called once', () => { + new mocked.mockConstructor(); + + expect(mocked.withConstructor()).wasCalled(1); + }); + + it('should fail when function has been called multiple times', () => { + new mocked.mockConstructor(); + new mocked.mockConstructor(); + new mocked.mockConstructor(); + + verifyFailure( + mocked.withConstructor(), + matchers.wasCalled(), + `Expected constructor to be called 1 times but it was called 3 times with matching parameters and 3 times in total.`, + 1, + ); + }); + + it('should fail when function has not been called', () => { + verifyFailure( + mocked.withConstructor(), + matchers.wasCalled(), + `Expected constructor to be called 1 times but it was called 0 times with matching parameters and 0 times in total.`, + 1, + ); + }); + }); + + describe('wasCalled(2)', () => { + it('should fail when function has been called once', () => { + new mocked.mockConstructor(); + + verifyFailure( + mocked.withConstructor(), + matchers.wasCalled(), + `Expected constructor to be called 2 times but it was called 1 times with matching parameters and 1 times in total.`, + 2, + ); + }); + + it('should not fail when function has been called twice', () => { + new mocked.mockConstructor(); + new mocked.mockConstructor(); + + expect(mocked.withConstructor()).wasCalled(2); + }); + + it('should fail when function has been called multiple times', () => { + new mocked.mockConstructor(); + new mocked.mockConstructor(); + new mocked.mockConstructor(); + + verifyFailure( + mocked.withConstructor(), + matchers.wasCalled(), + `Expected constructor to be called 2 times but it was called 3 times with matching parameters and 3 times in total.`, + 2, + ); + }); + + it('should fail when function has not been called', () => { + verifyFailure( + mocked.withConstructor(), + matchers.wasCalled(), + `Expected constructor to be called 2 times but it was called 0 times with matching parameters and 0 times in total.`, + 2, + ); + }); + }); + }); + + describe('assertion with parameters', () => { + const paramOne = 'one'; + const paramTwo = 123; + + beforeEach(() => { + mocked.setup(setupConstructor()); + }); + + describe('wasCalledAtLeastOnce()', () => { + it('should not fail when function has been called once with matching params', () => { + new mocked.mockConstructor(paramOne, paramTwo); + + expect(mocked.withConstructor().withParameters(paramOne, paramTwo)).wasCalledAtLeastOnce(); + }); + + it(`should fail when function has been called once with "two" instead of "one"`, () => { + new mocked.mockConstructor('two', paramTwo); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasCalledAtLeastOnce(), + `Expected constructor to be called with params ["one", 123] but it was only called with these parameters:\n[\n["two",123]\n]`, + ); + }); + + it(`should fail when function has been called once with "456" instead of "123"`, () => { + new mocked.mockConstructor(paramOne, 456); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, 123), + matchers.wasCalledAtLeastOnce(), + `Expected constructor to be called with params ["one", 123] but it was only called with these parameters:\n[\n["one",456]\n]`, + ); + }); + + it('should fail when function has been called once with missing parameters', () => { + new mocked.mockConstructor(paramOne); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasCalledAtLeastOnce(), + `Expected constructor to be called with params ["one", 123] but it was only called with these parameters:\n[\n["one"]\n]`, + ); + }); + + it('should not fail when function has been called multiple times', () => { + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + + expect(mocked.withConstructor().withParameters(paramOne, paramTwo)).wasCalledAtLeastOnce(); + }); + + it('should fail when function has not been called', () => { + verifyFailure( + mocked.withConstructor().withParameters('one', 123), + matchers.wasCalledAtLeastOnce(), + `Expected constructor to be called with params ["one", 123] but it was not.`, + ); + }); + }); + + describe('wasNotCalled()', () => { + it('should fail when function has been called once with matching parameters', () => { + new mocked.mockConstructor(paramOne, paramTwo); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasNotCalled(), + `Expected constructor to be called 0 times with params ["one", 123] but it was called 1 times with matching parameters and 1 times in total.\n[\n["one",123]\n]`, + ); + }); + + it('should not fail when function has been called once with different parameters', () => { + new mocked.mockConstructor('two', paramTwo); + + expect(mocked.withConstructor().withParameters(paramOne, paramTwo)).wasNotCalled(); + }); + + it('should fail when function has been called multiple times with matching params', () => { + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasNotCalled(), + `Expected constructor to be called 0 times with params ["one", 123] but it was called 3 times with matching parameters and 3 times in total.\n[\n[\"one\",123]\n[\"one\",123]\n[\"one\",123]\n]`, + ); + }); + + it('should not fail when function has not been called', () => { + expect(mocked.withConstructor().withParameters(paramOne, paramTwo)).wasNotCalled(); + }); + }); + + describe('wasCalledOnce()', () => { + it('should not fail when function has been called once with matching parameters', () => { + new mocked.mockConstructor(paramOne, paramTwo); + + expect(mocked.withConstructor().withParameters(paramOne, paramTwo)).wasCalledOnce(); + }); + + it('should not fail when function has been called once with matching parameters and many times with other params', () => { + new mocked.mockConstructor(paramOne, paramTwo); + + new mocked.mockConstructor('two', paramTwo); + new mocked.mockConstructor('two', paramTwo); + new mocked.mockConstructor('two', paramTwo); + + expect(mocked.withConstructor().withParameters(paramOne, paramTwo)).wasCalledOnce(); + }); + + it('should fail when function has been called multiple times', () => { + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasCalledOnce(), + `Expected constructor to be called 1 times with params ["one", 123] but it was called 3 times with matching parameters and 3 times in total.\n[\n[\"one\",123]\n[\"one\",123]\n[\"one\",123]\n]`, + ); + }); + + it('should fail when function has not been called with matching params but multiple times with other params', () => { + new mocked.mockConstructor('two', paramTwo); + new mocked.mockConstructor('two', paramTwo); + new mocked.mockConstructor('two', paramTwo); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasCalledOnce(), + `Expected constructor to be called 1 times with params ["one", 123] but it was called 0 times with matching parameters and 3 times in total.\n[\n[\"two\",123]\n[\"two\",123]\n[\"two\",123]\n]`, + ); + }); + }); + + describe('wasCalled(2)', () => { + it('should fail when function has been called once with correct parameters', () => { + new mocked.mockConstructor(paramOne, paramTwo); + + new mocked.mockConstructor('two', paramTwo); + new mocked.mockConstructor('two', paramTwo); + new mocked.mockConstructor('two', paramTwo); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasCalled(), + `Expected constructor to be called 2 times with params ["one", 123] but it was called 1 times with matching parameters and 4 times in total.\n[\n[\"one\",123]\n[\"two\",123]\n[\"two\",123]\n[\"two\",123]\n]`, + 2, + ); + }); + + it('should fail when function has been called twice with incorrect params', () => { + new mocked.mockConstructor('two', paramTwo); + new mocked.mockConstructor('two', paramTwo); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasCalled(), + `Expected constructor to be called 2 times with params ["one", 123] but it was called 0 times with matching parameters and 2 times in total.\n[\n[\"two\",123]\n[\"two\",123]\n]`, + 2, + ); + }); + + it('should not fail when function has been called twice with correct params and multiple times with different params', () => { + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + + new mocked.mockConstructor('two', paramTwo); + new mocked.mockConstructor('two', paramTwo); + + expect(mocked.withConstructor().withParameters(paramOne, paramTwo)).wasCalled(2); + }); + + it('should fail when function has been called multiple times with matching params', () => { + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + new mocked.mockConstructor(paramOne, paramTwo); + + verifyFailure( + mocked.withConstructor().withParameters(paramOne, paramTwo), + matchers.wasCalled(), + `Expected constructor to be called 2 times with params ["one", 123] but it was called 3 times with matching parameters and 3 times in total.\n[\n[\"one\",123]\n[\"one\",123]\n[\"one\",123]\n]`, + 2, + ); + }); + }); + }); + + describe('assertion with params and explicit', () => { + beforeEach(() => { + mocked.setup(setupConstructor()); + }); + + describe('wasNotCalled()', () => { + it('should fail when function has been called once with matching parameters', () => { + new mocked.mockConstructor('one', 123, true); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasNotCalled(), + `Expected constructor to be called 0 times with params ["one", 123, true] and 0 times with any other parameters but it was called 1 times with matching parameters and 1 times in total.\n[\n["one",123,true]\n]`, + ); + }); + + it('should fail when function has been called once with different parameters', () => { + new mocked.mockConstructor('two', 123, true); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasNotCalled(), + `Expected constructor to be called 0 times with params ["one", 123, true] and 0 times with any other parameters but it was called 0 times with matching parameters and 1 times in total.\n[\n["two",123,true]\n]`, + ); + }); + + it('should fail when function has been called multiple times with matching params', () => { + new mocked.mockConstructor('one', 123, true); + new mocked.mockConstructor('one', 123, true); + new mocked.mockConstructor('one', 123, true); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasNotCalled(), + `Expected constructor to be called 0 times with params ["one", 123, true] and 0 times with any other parameters but it was called 3 times with matching parameters and 3 times in total.\n[\n[\"one\",123,true]\n[\"one\",123,true]\n[\"one\",123,true]\n]`, + ); + }); + + it('should not fail when function has not been called', () => { + expect(mocked.withConstructor().withParameters('one', 123, true).strict()).wasNotCalled(); + }); + }); + + describe('wasCalledOnce()', () => { + it('should not fail when function has been called once with matching parameters', () => { + new mocked.mockConstructor('one', 123, true); + + expect(mocked.withConstructor().withParameters('one', 123, true).strict()).wasCalledOnce(); + }); + + it('should fail when function has been called once with matching parameters and many times with other params', () => { + new mocked.mockConstructor('one', 123, true); + + new mocked.mockConstructor('two', 123, true); + new mocked.mockConstructor('one', 456, true); + new mocked.mockConstructor('one', 456, false); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasCalledOnce(), + `Expected constructor to be called 1 times with params ["one", 123, true] and 0 times with any other parameters but it was called 1 times with matching parameters and 4 times in total.\n[\n[\"one\",123,true]\n[\"two\",123,true]\n[\"one\",456,true]\n[\"one\",456,false]\n]`, + ); + }); + + it('should fail when function has been called multiple times', () => { + new mocked.mockConstructor('one', 123, true); + new mocked.mockConstructor('one', 123, true); + new mocked.mockConstructor('one', 123, true); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasCalledOnce(), + `Expected constructor to be called 1 times with params ["one", 123, true] and 0 times with any other parameters but it was called 3 times with matching parameters and 3 times in total.\n[\n[\"one\",123,true]\n[\"one\",123,true]\n[\"one\",123,true]\n]`, + ); + }); + + it('should fail when function has not been called with matching params but multiple times with other params', () => { + new mocked.mockConstructor('two', 123, true); + new mocked.mockConstructor('one', 456, true); + new mocked.mockConstructor('one', 456, false); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasCalledOnce(), + `Expected constructor to be called 1 times with params ["one", 123, true] and 0 times with any other parameters but it was called 0 times with matching parameters and 3 times in total.\n[\n[\"two\",123,true]\n[\"one\",456,true]\n[\"one\",456,false]\n]`, + ); + }); + }); + + describe('wasCalled(2)', () => { + it('should fail when function has been called once with correct parameters', () => { + new mocked.mockConstructor('one', 123, true); + + new mocked.mockConstructor('two', 123, true); + new mocked.mockConstructor('one', 456, true); + new mocked.mockConstructor('one', 456, false); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasCalled(), + `Expected constructor to be called 2 times with params ["one", 123, true] and 0 times with any other parameters but it was called 1 times with matching parameters and 4 times in total.\n[\n[\"one\",123,true]\n[\"two\",123,true]\n[\"one\",456,true]\n[\"one\",456,false]\n]`, + 2, + ); + }); + + it('should fail when function has been called twice with incorrect params', () => { + new mocked.mockConstructor('two', 123, true); + new mocked.mockConstructor('one', 456, true); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasCalled(), + `Expected constructor to be called 2 times with params ["one", 123, true] and 0 times with any other parameters but it was called 0 times with matching parameters and 2 times in total.\n[\n[\"two\",123,true]\n[\"one\",456,true]\n]`, + 2, + ); + }); + + it('should fail when function has been called twice with correct params and multiple times with different params', () => { + new mocked.mockConstructor('one', 123, true); + new mocked.mockConstructor('one', 123, true); + + new mocked.mockConstructor('two', 123, true); + new mocked.mockConstructor('one', 456, true); + new mocked.mockConstructor('one', 456, false); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasCalled(), + `Expected constructor to be called 2 times with params ["one", 123, true] and 0 times with any other parameters but it was called 2 times with matching parameters and 5 times in total.\n[\n[\"one\",123,true]\n[\"one\",123,true]\n[\"two\",123,true]\n[\"one\",456,true]\n[\"one\",456,false]\n]`, + 2, + ); + }); + + it('should fail when function has been called multiple times with matching params', () => { + new mocked.mockConstructor('one', 123, true); + new mocked.mockConstructor('one', 123, true); + new mocked.mockConstructor('one', 123, true); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasCalled(), + `Expected constructor to be called 2 times with params ["one", 123, true] and 0 times with any other parameters but it was called 3 times with matching parameters and 3 times in total.\n[\n[\"one\",123,true]\n[\"one\",123,true]\n[\"one\",123,true]\n]`, + 2, + ); + }); + + it('should fail when function has not been called with matching params but has been called with different params', () => { + new mocked.mockConstructor('two', 123, true); + new mocked.mockConstructor('one', 456, true); + new mocked.mockConstructor('one', 456, false); + + verifyFailure( + mocked.withConstructor().withParameters('one', 123, true).strict(), + matchers.wasCalled(), + `Expected constructor to be called 2 times with params ["one", 123, true] and 0 times with any other parameters but it was called 0 times with matching parameters and 3 times in total.\n[\n[\"two\",123,true]\n[\"one\",456,true]\n[\"one\",456,false]\n]`, + 2, + ); + }); + }); + }); + }); + describe('withGetter', () => { it('called directly on mock instance', () => { mocked.setup(setupProperty('propertyOne')); @@ -886,7 +1471,7 @@ describe('mock', () => { }); it('called on checker returned from setup function', () => { - const verifier = mocked.setupProperty('propertyOne'); + const verifier = mocked.setupProperty('propertyOne').getter; get(mock.propertyOne); @@ -1071,7 +1656,7 @@ describe('mock', () => { }); it('should count function calls the same regardless of called via mock or constructor', () => { - const constructedInstance = new mocked.mockConstructor({}, new Date()); + const constructedInstance = new mocked.mockConstructor(); get(mock.propertyOne); get(constructedInstance.propertyOne); @@ -1098,24 +1683,60 @@ describe('mock', () => { }); describe('defineProperty', () => { + describe('getter', () => { + it('called directly on mock instance', () => { + mocked.setup(defineProperty('propertyOne')); + + get(mock.propertyOne); + + expect(mocked.withGetter('propertyOne')).wasCalledAtLeastOnce(); + }); + + it('called on checker returned from setup function', () => { + const verifier = mocked.defineProperty('propertyOne').getter; + + get(mock.propertyOne); + + expect(verifier).wasCalledAtLeastOnce(); + }); + }); + + describe('setter', () => { + it('called directly on mock instance', () => { + mocked.setup(defineProperty('propertyOne')); + + mock.propertyOne = 'one'; + + expect(mocked.withSetter('propertyOne')).wasCalledAtLeastOnce(); + }); + + it('called on checker returned from setup function', () => { + const verifier = mocked.defineProperty('propertyOne').setter; + + mock.propertyOne = 'one'; + + expect(verifier).wasCalledAtLeastOnce(); + }); + }); + }); + + describe('withSetter', () => { it('called directly on mock instance', () => { - mocked.setup(defineProperty('propertyOne')); + mocked.setup(setupProperty('propertyOne')); - get(mock.propertyOne); + mock.propertyOne = 'one'; - expect(mocked.withGetter('propertyOne')).wasCalledAtLeastOnce(); + expect(mocked.withSetter('propertyOne')).wasCalledAtLeastOnce(); }); it('called on checker returned from setup function', () => { - const verifier = mocked.defineProperty('propertyOne'); + const verifier = mocked.setupProperty('propertyOne').setter; - get(mock.propertyOne); + mock.propertyOne = 'one'; expect(verifier).wasCalledAtLeastOnce(); }); - }); - describe('withSetter', () => { describe('assertion with parameters', () => { beforeEach(() => { mocked.setup(defineProperty('propertyOne')); @@ -1337,7 +1958,7 @@ describe('mock', () => { }); it('should count function calls the same regardless of called via mock or constructor', () => { - const constructedInstance = new mocked.mockConstructor({}, new Date()); + const constructedInstance = new mocked.mockConstructor(); mock.propertyOne = 'one'; constructedInstance.propertyOne = 'one'; @@ -2077,7 +2698,7 @@ class SampleMockedClass { public propertyTwo = 123; public propertyThree = true; - constructor(_paramsOne: {}, _paramTwo: Date) {} + constructor(_paramOne?: string, _paramTwo?: number, _paramThree?: boolean) {} public functionWithNoParamsAndNoReturn(): void {}