From 82b31526118ca2292a99cc89aaadc5be19d88cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Wed, 6 May 2020 18:54:23 +0200 Subject: [PATCH 01/14] add generics correctly to Arg.is and Arg.all --- src/Arguments.ts | 8 ++++---- src/Transformations.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Arguments.ts b/src/Arguments.ts index eb4ce4d..c722f27 100644 --- a/src/Arguments.ts +++ b/src/Arguments.ts @@ -18,7 +18,7 @@ export class Argument { } } -export class AllArguments extends Argument { +export class AllArguments extends Argument { constructor() { super('{all}', () => true); } @@ -27,8 +27,8 @@ export class AllArguments extends Argument { export class Arg { private static _all: AllArguments; - static all() { - return this._all = (this._all || new AllArguments()); + static all() { + return this._all = new AllArguments(); } static any(): Argument & any @@ -51,7 +51,7 @@ export class Arg { }); } - static is(predicate: (input: any) => boolean): Argument & T { + static is(predicate: (input: T) => boolean): Argument & T { return new Argument('{predicate ' + this.toStringify(predicate) + '}', predicate) as Argument & T; } diff --git a/src/Transformations.ts b/src/Transformations.ts index 921bd4a..f1bf74e 100644 --- a/src/Transformations.ts +++ b/src/Transformations.ts @@ -41,7 +41,7 @@ type FunctionHandler = export type FunctionSubstitute = ((...args: TArguments) => (TReturnType & MockObjectMixin)) & - ((allArguments: AllArguments) => (TReturnType & MockObjectMixin)) + ((allArguments: AllArguments) => (TReturnType & MockObjectMixin)) export type NoArgumentFunctionSubstitute = (() => (TReturnType & NoArgumentMockObjectMixin)) export type PropertySubstitute = (TReturnType & Partial>); @@ -70,7 +70,7 @@ export type ObjectSubstitute = ObjectSub mimick(instance: T): void; } -type TerminatingFunction = ((...args: TArguments) => void) & ((arg: AllArguments) => void) +type TerminatingFunction = ((...args: TArguments) => void) & ((arg: AllArguments) => void) type TerminatingObject = { [P in keyof T]: From e4d164bbcba56430b48a567e278cfef454c97946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Mon, 11 May 2020 01:05:53 +0200 Subject: [PATCH 02/14] rework arguments and implement .not --- src/Arguments.ts | 116 +++++++++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 38 deletions(-) diff --git a/src/Arguments.ts b/src/Arguments.ts index c722f27..81f4ab8 100644 --- a/src/Arguments.ts +++ b/src/Arguments.ts @@ -1,61 +1,55 @@ -export class Argument { - +type PredicateFunction = (arg: T) => boolean +type ArgumentOptions = { + inverseMatch?: boolean +} +class BaseArgument { constructor( - private description: string, - private matchingFunction: (arg: T) => boolean + private _description: string, + private _matchingFunction: PredicateFunction, + private _options?: ArgumentOptions ) { } matches(arg: T) { - return this.matchingFunction(arg); + const inverseMatch = this._options?.inverseMatch ?? false + return inverseMatch ? !this._matchingFunction(arg) : this._matchingFunction(arg); } toString() { - return this.description; + return this._description; } [Symbol.for('nodejs.util.inspect.custom')]() { - return this.description; + return this._description; } } -export class AllArguments extends Argument { - constructor() { - super('{all}', () => true); +export class Argument extends BaseArgument { + private readonly _type = 'SingleArgument' + constructor(description: string, matchingFunction: PredicateFunction, options?: ArgumentOptions) { + super(description, matchingFunction, options) + } + get type(): 'SingleArgument' { + return this._type } } -export class Arg { - private static _all: AllArguments; - - static all() { - return this._all = new AllArguments(); +export class AllArguments extends BaseArgument { + private readonly _type = 'AllArguments' + constructor() { + super('{all}', () => true, {}); } - - static any(): Argument & any - static any(type: T): Argument & string - static any(type: T): Argument & number - static any(type: T): Argument & boolean - static any(type: T): Argument & any[] - static any(type: T): Argument & Function - static any(type: T): Argument & any - static any(type?: string): Argument & any { - const description = !type ? '{any arg}' : '{type ' + type + '}'; - return new Argument(description, x => { - if (!type) - return true; - - if (type === 'array') - return Array.isArray(x); - - return typeof x === type; - }); + get type(): 'AllArguments' { + return this._type } +} - static is(predicate: (input: T) => boolean): Argument & T { - return new Argument('{predicate ' + this.toStringify(predicate) + '}', predicate) as Argument & T; - } +type ExtractFirstArg = T extends AllArguments ? TArgs[0] : T - private static toStringify(obj: any) { +export namespace Arg { + type ReturnArg = Argument & T; + type Inversable = T & { not: T } + const factory = (factoryF: Function) => (...args: any[]): T => factoryF(...args) + const toStringify = (obj: any) => { if (typeof obj.inspect === 'function') return obj.inspect(); @@ -64,4 +58,50 @@ export class Arg { return obj; } + + + export const all = (): AllArguments => new AllArguments(); + + type Is = (predicate: PredicateFunction>) => ReturnArg> + const isFunction = >(predicate: T, options?: ArgumentOptions) => new Argument( + `{predicate ${toStringify(predicate)}}`, predicate, options + ); + + const isArgFunction: Inversable = (predicate) => factory(isFunction)(predicate); + isArgFunction.not = (predicate) => factory(isFunction)(predicate, { inverseMatch: true }); + export const is = isArgFunction + + type MapAnyReturn = T extends 'any' ? + ReturnArg : T extends 'string' ? + ReturnArg : T extends 'number' ? + ReturnArg : T extends 'boolean' ? + ReturnArg : T extends 'symbol' ? + ReturnArg : T extends 'undefined' ? + ReturnArg : T extends 'object' ? + ReturnArg : T extends 'function' ? + ReturnArg : T extends 'array' ? + ReturnArg : any; + + type AnyType = 'string' | 'number' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | 'array' | 'any'; + type Any = (type?: T) => MapAnyReturn; + + const anyFunction = (type: AnyType = 'any', options?: ArgumentOptions) => { + const description = !type ? '{any arg}' : `{type ${type}}`; + const predicate = (x: any) => { + switch (type) { + case 'any': + return true; + case 'array': + return Array.isArray(x); + default: + return typeof x === type; + } + } + + return new Argument(description, predicate, options); + } + + const anyArgFunction: Inversable = (type) => factory(anyFunction)(type); + anyArgFunction.not = (type) => factory(anyFunction)(type, { inverseMatch: true }); + export const any = anyArgFunction; } \ No newline at end of file From f33d83e8535c7e740b2263eaefc6a9af2740e2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Mon, 11 May 2020 01:06:16 +0200 Subject: [PATCH 03/14] add some arguments test --- spec/Utilities.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/Utilities.spec.ts b/spec/Utilities.spec.ts index d80da42..edaa16d 100644 --- a/spec/Utilities.spec.ts +++ b/spec/Utilities.spec.ts @@ -77,6 +77,8 @@ test('areArgumentArraysEqual should return valid result using Arg.any()', t => { t.true(areArgumentArraysEqual([Arg.any('string')], ['foo'])); t.true(areArgumentArraysEqual([Arg.any('number')], [1])); t.true(areArgumentArraysEqual([Arg.any('boolean')], [true])); + t.true(areArgumentArraysEqual([Arg.any('symbol')], [Symbol()])); + t.true(areArgumentArraysEqual([Arg.any('undefined')], [undefined])); t.true(areArgumentArraysEqual([Arg.any('object')], [testObject])); t.true(areArgumentArraysEqual([Arg.any('array')], [testArray])); t.true(areArgumentArraysEqual([Arg.any('function')], [testFunc])); From e667b075be83dd3088b16907fa04ca433406c101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Thu, 14 May 2020 21:24:24 +0200 Subject: [PATCH 04/14] upgrade dependencies --- package-lock.json | 60 +++++++++++++++++++++++------------------------ package.json | 6 ++--- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index f936026..388c85f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -185,9 +185,9 @@ "dev": true }, "@types/node": { - "version": "13.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.5.tgz", - "integrity": "sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz", + "integrity": "sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA==", "dev": true }, "@types/normalize-package-data": { @@ -197,9 +197,9 @@ "dev": true }, "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", "dev": true }, "acorn-walk": { @@ -333,9 +333,9 @@ "dev": true }, "ava": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/ava/-/ava-3.8.1.tgz", - "integrity": "sha512-OPWrTxcf1EbtAaGGFQPLbx4AaVqPrFMumKOKn2SzIRo+RTKb33lF2aoVnWqBeZaJ68uSc9R6jqIE7qkG6O33uQ==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/ava/-/ava-3.8.2.tgz", + "integrity": "sha512-sph3oUsVTGsq4qbgeWys03QKCmXjkZUO3oPnFWXEW6g1SReCY9vuONGghMgw1G6VOzkg1k+niqJsOzwfO8h9Ng==", "dev": true, "requires": { "@concordance/react": "^2.0.0", @@ -369,7 +369,7 @@ "indent-string": "^4.0.0", "is-error": "^2.2.2", "is-plain-object": "^3.0.0", - "is-promise": "^3.0.0", + "is-promise": "^4.0.0", "lodash": "^4.17.15", "matcher": "^3.0.0", "md5-hex": "^3.0.1", @@ -380,12 +380,12 @@ "picomatch": "^2.2.2", "pkg-conf": "^3.1.0", "plur": "^4.0.0", - "pretty-ms": "^6.0.1", + "pretty-ms": "^7.0.0", "read-pkg": "^5.2.0", "resolve-cwd": "^3.0.0", "slash": "^3.0.0", "source-map-support": "^0.5.19", - "stack-utils": "^2.0.1", + "stack-utils": "^2.0.2", "strip-ansi": "^6.0.0", "supertap": "^1.0.0", "temp-dir": "^2.0.0", @@ -408,9 +408,9 @@ "dev": true }, "blueimp-md5": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.14.0.tgz", - "integrity": "sha512-fhX8JsIgugJ39g9MUJ4Y0S+WYd/1HATNVzW4nEVknP5uJU1mA7LZCV3OuVH9OvxpuYQXu6ttst0IYIlAyVfBQg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.15.0.tgz", + "integrity": "sha512-Zc6sowqlCWu3+V0bocZwdaPPXlRv14EHtYcQDCOghj9EdyKLMkAOODBh3HHAx5r7QRylDYCOaXa/b/edgBLDpA==", "dev": true }, "boxen": { @@ -941,9 +941,9 @@ } }, "fastq": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.7.0.tgz", - "integrity": "sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -1283,9 +1283,9 @@ } }, "is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-3.0.0.tgz", - "integrity": "sha512-aTHJ4BvETyySzLhguH+7sL4b8765eecqq7ZrHVuhZr3FjCL/IV+LsvisEeH+9d0AkChYny3ad1KEL+mKy4ot7A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true }, "is-typedarray": { @@ -1878,9 +1878,9 @@ "dev": true }, "pretty-ms": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-6.0.1.tgz", - "integrity": "sha512-ke4njoVmlotekHlHyCZ3wI/c5AMT8peuHs8rKJqekj/oR5G8lND2dVpicFlUz5cbZgE290vvkMuDwfj/OcW1kw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.0.tgz", + "integrity": "sha512-J3aPWiC5e9ZeZFuSeBraGxSkGMOvulSWsxDByOcbD1Pr75YL3LSNIKIb52WXbCLE1sS5s4inBBbryjF4Y05Ceg==", "dev": true, "requires": { "parse-ms": "^2.1.0" @@ -2149,9 +2149,9 @@ "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -2317,9 +2317,9 @@ } }, "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.2.tgz", + "integrity": "sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==", "dev": true }, "unique-string": { diff --git a/package.json b/package.json index e3a4060..39bdf77 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "dependencies": {}, "devDependencies": { "@ava/typescript": "^1.1.0", - "ava": "^3.7.0", - "typescript": "^3.0.0" + "ava": "^3.8.2", + "typescript": "^3.9.2" }, "ava": { "typescript": { @@ -46,4 +46,4 @@ "failFast": true, "failWithoutAssertions": true } -} \ No newline at end of file +} From ffa44c24131df91b9af4802e5667be82fa69df38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Thu, 14 May 2020 21:24:41 +0200 Subject: [PATCH 05/14] add Arguments spec --- spec/Arguments.spec.ts | 111 +++++++++++++++++++++++++++++++++++++++++ spec/Utilities.spec.ts | 6 +-- 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 spec/Arguments.spec.ts diff --git a/spec/Arguments.spec.ts b/spec/Arguments.spec.ts new file mode 100644 index 0000000..6efa1c6 --- /dev/null +++ b/spec/Arguments.spec.ts @@ -0,0 +1,111 @@ +import test from 'ava'; +import { Arg } from '../src'; +import { Argument } from 'src/Arguments'; + +const testObject = { "foo": "bar" }; +const testArray = ["a", 1, true]; + +const parent = {} as any; +parent.child = parent; +const root = {} as any; +root.path = { to: { nested: root } }; +const testFunc = () => { }; + +test('should match any argument(s) using Arg.all', t => { + t.true(Arg.all().matches([])); + t.true(Arg.all().matches([0])); + t.true(Arg.all().matches([1])); + t.true(Arg.all().matches(['string'])); + t.true(Arg.all().matches([true])); + t.true(Arg.all().matches([false])); + t.true(Arg.all().matches(null)); + t.true(Arg.all().matches(undefined)); + t.true(Arg.all().matches([1, 2])); + t.true(Arg.all().matches(['string1', 'string2'])); +}) + +test('should match any argument using Arg.any', t => { + t.true(Arg.any().matches('hi')); + t.true(Arg.any().matches(1)); + t.true(Arg.any().matches(0)); + t.true(Arg.any().matches(false)); + t.true(Arg.any().matches(true)); + t.true(Arg.any().matches(null)); + t.true(Arg.any().matches(undefined)); + t.true(Arg.any().matches(testObject)); + t.true(Arg.any().matches(testArray)); + t.true(Arg.any().matches(testFunc)); + t.true(Arg.any().matches()); + t.true(Arg.any().matches(parent)); + t.true(Arg.any().matches(root)); + t.true(Arg.any().matches(parent)); + t.true(Arg.any().matches(root)); +}); + +test('should not match any argument using Arg.any.not', t => { + t.false(Arg.any.not().matches('hi')); + t.false(Arg.any.not().matches(1)); + t.false(Arg.any.not().matches(0)); + t.false(Arg.any.not().matches(false)); + t.false(Arg.any.not().matches(true)); + t.false(Arg.any.not().matches(null)); + t.false(Arg.any.not().matches(undefined)); + t.false(Arg.any.not().matches(testObject)); + t.false(Arg.any.not().matches(testArray)); + t.false(Arg.any.not().matches(testFunc)); + t.false(Arg.any.not().matches()); + t.false(Arg.any.not().matches(parent)); + t.false(Arg.any.not().matches(root)); + t.false(Arg.any.not().matches(parent)); + t.false(Arg.any.not().matches(root)); +}); + +test('should match the type of the argument using Arg.any', t => { + t.true(Arg.any('string').matches('foo')); + t.true(Arg.any('number').matches(1)); + t.true(Arg.any('boolean').matches(true)); + t.true(Arg.any('symbol').matches(Symbol())); + t.true((>Arg.any('undefined')).matches(undefined)); + t.true(Arg.any('object').matches(testObject)); + t.true(Arg.any('array').matches(testArray)); + t.true(Arg.any('function').matches(testFunc)); + t.true(Arg.any('object').matches(parent)); + t.true(Arg.any('object').matches(root)); + + t.false((>Arg.any('string')).matches(1)); + t.false((>Arg.any('number')).matches('string')); + t.false(Arg.any('boolean').matches(null)); + t.false((>Arg.any('object')).matches('foo')); + t.false((>Arg.any('array')).matches('bar')); + t.false((>Arg.any('function')).matches('foo')); +}); + + +test('should not match the type of the argument using Arg.any.not', t => { + t.false(Arg.any.not('string').matches('123')); + t.false(Arg.any.not('number').matches(123)); + t.false(Arg.any.not('boolean').matches(true)); + t.false(Arg.any.not('symbol').matches(Symbol())); + t.false((>Arg.any.not('undefined')).matches(undefined)); + t.false(Arg.any.not('object').matches(testObject)); + t.false(Arg.any.not('array').matches(testArray)); + t.false(Arg.any.not('function').matches(testFunc)); + t.false(Arg.any.not('object').matches(parent)); + t.false(Arg.any.not('object').matches(root)); +}); + +test('should match the argument with the predicate function using Arg.is', t => { + t.true(Arg.is(x => x === 'foo').matches('foo')); + t.true(Arg.is(x => x % 2 == 0).matches(4)); + + t.false(Arg.is(x => x === 'foo').matches('bar')); + t.false(Arg.is(x => x % 2 == 0).matches(3)); +}); + +test('should not match the argument with the predicate function using Arg.is.not', t => { + t.false(Arg.is.not(x => x === 'foo').matches('foo')); + t.false(Arg.is.not(x => x % 2 == 0).matches(4)); + + t.true(Arg.is.not(x => x === 'foo').matches('bar')); + t.true(Arg.is.not(x => x % 2 == 0).matches(3)); +}); \ No newline at end of file diff --git a/spec/Utilities.spec.ts b/spec/Utilities.spec.ts index edaa16d..be6ea2b 100644 --- a/spec/Utilities.spec.ts +++ b/spec/Utilities.spec.ts @@ -59,7 +59,7 @@ test('areArgumentArraysEqual should return valid result using Arg.all()', t => { t.true(areArgumentArraysEqual([Arg.all()], [parent, root])); }) -test('areArgumentArraysEqual should return valid result using Arg.any()', t => { +test('areArgumentArraysEqual should return valid result using Arg', t => { t.true(areArgumentArraysEqual([Arg.any()], ['hi'])); t.true(areArgumentArraysEqual([Arg.any()], [1])); t.true(areArgumentArraysEqual([Arg.any()], [0])); @@ -82,6 +82,8 @@ test('areArgumentArraysEqual should return valid result using Arg.any()', t => { t.true(areArgumentArraysEqual([Arg.any('object')], [testObject])); t.true(areArgumentArraysEqual([Arg.any('array')], [testArray])); t.true(areArgumentArraysEqual([Arg.any('function')], [testFunc])); + t.true(areArgumentArraysEqual([Arg.any('object')], [parent])); + t.true(areArgumentArraysEqual([Arg.any('object')], [root])); t.false(areArgumentArraysEqual([Arg.any('string')], [1])); t.false(areArgumentArraysEqual([Arg.any('number')], ['string'])); @@ -89,8 +91,6 @@ test('areArgumentArraysEqual should return valid result using Arg.any()', t => { t.false(areArgumentArraysEqual([Arg.any('object')], ['foo'])); t.false(areArgumentArraysEqual([Arg.any('array')], ['bar'])); t.false(areArgumentArraysEqual([Arg.any('function')], ['foo'])); - t.true(areArgumentArraysEqual([Arg.any('object')], [parent])); - t.true(areArgumentArraysEqual([Arg.any('object')], [root])); }) test('areArgumentArraysEqual should return valid result using Arg.is()', t => { From e3f37384d46ceeaf739f9e95d2316285661ce16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Thu, 14 May 2020 21:25:37 +0200 Subject: [PATCH 06/14] fix types to support ts 3.9 --- src/Arguments.ts | 20 +++++++++----------- src/Transformations.ts | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Arguments.ts b/src/Arguments.ts index 81f4ab8..5d03c33 100644 --- a/src/Arguments.ts +++ b/src/Arguments.ts @@ -24,28 +24,27 @@ class BaseArgument { } export class Argument extends BaseArgument { - private readonly _type = 'SingleArgument' + private readonly _type = 'SingleArgument'; constructor(description: string, matchingFunction: PredicateFunction, options?: ArgumentOptions) { - super(description, matchingFunction, options) + super(description, matchingFunction, options); } get type(): 'SingleArgument' { - return this._type + return this._type; } } -export class AllArguments extends BaseArgument { - private readonly _type = 'AllArguments' +export class AllArguments extends BaseArgument { + private readonly _type = 'AllArguments'; constructor() { super('{all}', () => true, {}); } get type(): 'AllArguments' { - return this._type + return this._type; } } -type ExtractFirstArg = T extends AllArguments ? TArgs[0] : T - export namespace Arg { + type ExtractFirstArg = T extends AllArguments ? TArgs[0] : T type ReturnArg = Argument & T; type Inversable = T & { not: T } const factory = (factoryF: Function) => (...args: any[]): T => factoryF(...args) @@ -59,8 +58,7 @@ export namespace Arg { return obj; } - - export const all = (): AllArguments => new AllArguments(); + export const all = (): AllArguments => new AllArguments(); type Is = (predicate: PredicateFunction>) => ReturnArg> const isFunction = >(predicate: T, options?: ArgumentOptions) => new Argument( @@ -83,7 +81,7 @@ export namespace Arg { ReturnArg : any; type AnyType = 'string' | 'number' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | 'array' | 'any'; - type Any = (type?: T) => MapAnyReturn; + type Any = (type?: T) => MapAnyReturn; const anyFunction = (type: AnyType = 'any', options?: ArgumentOptions) => { const description = !type ? '{any arg}' : `{type ${type}}`; diff --git a/src/Transformations.ts b/src/Transformations.ts index f1bf74e..c9df3fd 100644 --- a/src/Transformations.ts +++ b/src/Transformations.ts @@ -90,6 +90,6 @@ type ObjectSubstituteTransformation = { type Omit = Pick>; +// @ts-expect-error export type OmitProxyMethods = Omit; - export type DisabledSubstituteObject = T extends ObjectSubstitute, infer K> ? K : never; From f15d28bdb39aacd0e6d9df29a83d7132c5d4abe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Thu, 14 May 2020 21:50:21 +0200 Subject: [PATCH 07/14] add documentation for inverse matchers --- README.md | 113 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index ccb4490..c413e1a 100644 --- a/README.md +++ b/README.md @@ -25,35 +25,35 @@ interface Calculator { isEnabled: boolean; } -//Create: -var calculator = Substitute.for(); +// Create: +const calculator = Substitute.for(); -//Set a return value: +// Set a return value: calculator.add(1, 2).returns(3); -//Check received calls: +// Check received calls: calculator.received().add(1, Arg.any()); calculator.didNotReceive().add(2, 2); ``` ## Creating a mock -`var calculator = Substitute.for();` +`const calculator = Substitute.for();` ## Setting return types See the example below. The same syntax also applies to properties and fields. ```typescript -//single return type +// single return type calculator.add(1, 2).returns(4); -console.log(calculator.add(1, 2)); //prints 4 -console.log(calculator.add(1, 2)); //prints undefined +console.log(calculator.add(1, 2)); // prints 4 +console.log(calculator.add(1, 2)); // prints undefined -//multiple return types in sequence +// multiple return types in sequence calculator.add(1, 2).returns(3, 7, 9); -console.log(calculator.add(1, 2)); //prints 3 -console.log(calculator.add(1, 2)); //prints 7 -console.log(calculator.add(1, 2)); //prints 9 -console.log(calculator.add(1, 2)); //prints undefined +console.log(calculator.add(1, 2)); // prints 3 +console.log(calculator.add(1, 2)); // prints 7 +console.log(calculator.add(1, 2)); // prints 9 +console.log(calculator.add(1, 2)); // prints undefined ``` ## Working with promises @@ -61,25 +61,25 @@ When working with promises you can also use `resolves()` and `rejects()` to retu ```typescript calculator.heavyOperation(1, 2).resolves(4); -//same as calculator.heavyOperation(1, 2).returns(Promise.resolve(4)); -console.log(await calculator.heavyOperation(1, 2)); //prints 4 +// same as calculator.heavyOperation(1, 2).returns(Promise.resolve(4)); +console.log(await calculator.heavyOperation(1, 2)); // prints 4 ``` ```typescript calculator.heavyOperation(1, 2).rejects(new Error()); -//same as calculator.heavyOperation(1, 2).returns(Promise.reject(new Error())); -console.log(await calculator.heavyOperation(1, 2)); //throws Error +// same as calculator.heavyOperation(1, 2).returns(Promise.reject(new Error())); +console.log(await calculator.heavyOperation(1, 2)); // throws Error ``` ## Verifying calls ```typescript calculator.enabled = true; -var foo = calculator.add(1, 2); +const foo = calculator.add(1, 2); -//verify call to add(1, 2) +// verify call to add(1, 2) calculator.received().add(1, 2); -//verify property set to "true" +// verify property set to "true" calculator.received().enabled = true; ``` @@ -90,21 +90,38 @@ There are several ways of matching arguments. The examples below also applies to ```typescript import { Arg } from '@fluffy-spoon/substitute'; -//ignoring first argument +// ignoring first argument calculator.add(Arg.any(), 2).returns(10); -console.log(calculator.add(1337, 3)); //prints undefined since second argument doesn't match -console.log(calculator.add(1337, 2)); //prints 10 since second argument matches +console.log(calculator.add(1337, 3)); // prints undefined since second argument doesn't match +console.log(calculator.add(1337, 2)); // prints 10 since second argument matches -//received call with first arg 1 and second arg less than 0 +// received call with first arg 1 and second arg less than 0 calculator.received().add(1, Arg.is(x => x < 0)); ``` +### Generic and inverse matchers +```typescript +import { Arg } from '@fluffy-spoon/substitute'; + +const equalToZero = x => x === 0; + +// first argument will match any number +// second argument will match a number that is not '0' +calculator.divide(Arg.any('number'), Arg.is.not(equalToZero)).returns(10); +console.log(calculator.divide(100, 10)); // prints 10 + +const argIsNotZero = Arg.is.not(equalToZero); +calculator.received(1).divide(argIsNotZero, argIsNotZero); +``` + +> #### Note: `Arg.is()` will automatically infer the type of the argument it's replacing + ### Ignoring all arguments ```typescript -//ignoring all arguments +// ignoring all arguments calculator.add(Arg.all()).returns(10); -console.log(calculator.add(1, 3)); //prints 10 -console.log(calculator.add(5, 2)); //prints 10 +console.log(calculator.add(1, 3)); // prints 10 +console.log(calculator.add(5, 2)); // prints 10 ``` ### Match order @@ -113,15 +130,15 @@ The order of argument matchers matters. The first matcher that matches will alwa ```typescript calculator.add(Arg.all()).returns(10); calculator.add(1, 3).returns(1337); -console.log(calculator.add(1, 3)); //prints 10 -console.log(calculator.add(5, 2)); //prints 10 +console.log(calculator.add(1, 3)); // prints 10 +console.log(calculator.add(5, 2)); // prints 10 ``` ```typescript calculator.add(1, 3).returns(1337); calculator.add(Arg.all()).returns(10); -console.log(calculator.add(1, 3)); //prints 1337 -console.log(calculator.add(5, 2)); //prints 10 +console.log(calculator.add(1, 3)); // prints 1337 +console.log(calculator.add(5, 2)); // prints 10 ``` ## Partial mocks @@ -136,26 +153,26 @@ class RealCalculator implements Calculator { divide(a: number, b: number) => a / b; } -var realCalculator = new RealCalculator(); -var fakeCalculator = Substitute.for(); +const realCalculator = new RealCalculator(); +const fakeCalculator = Substitute.for(); -//let the subtract method always use the real method +// let the subtract method always use the real method fakeCalculator.subtract(Arg.all()).mimicks(realCalculator.subtract); -console.log(fakeCalculator.subtract(20, 10)); //prints 10 -console.log(fakeCalculator.subtract(1, 2)); //prints -1 +console.log(fakeCalculator.subtract(20, 10)); // prints 10 +console.log(fakeCalculator.subtract(1, 2)); // prints -1 -//for the add method, we only use the real method when the first arg is less than 10 -//else, we always return 1337 +// for the add method, we only use the real method when the first arg is less than 10 +// else, we always return 1337 fakeCalculator.add(Arg.is(x < 10), Arg.any()).mimicks(realCalculator.add); fakeCalculator.add(Arg.is(x >= 10), Arg.any()).returns(1337); -console.log(fakeCalculator.add(5, 100)); //prints 105 via real method -console.log(fakeCalculator.add(210, 7)); //prints 1337 via fake method +console.log(fakeCalculator.add(5, 100)); // prints 105 via real method +console.log(fakeCalculator.add(210, 7)); // prints 1337 via fake method -//for the divide method, we only use the real method for explicit arguments +// for the divide method, we only use the real method for explicit arguments fakeCalculator.divide(10, 2).mimicks(realCalculator.divide); fakeCalculator.divide(Arg.all()).returns(1338); -console.log(fakeCalculator.divide(10, 5)); //prints 5 -console.log(fakeCalculator.divide(9, 5)); //prints 1338 +console.log(fakeCalculator.divide(10, 5)); // prints 5 +console.log(fakeCalculator.divide(9, 5)); // prints 1338 ``` ## Throwing exceptions @@ -196,15 +213,15 @@ class Example { } } -var fake = Substitute.for(); +const fake = Substitute.for(); -//BAD: this would have called substitute.js' "received" method. -//fake.received(2); +// BAD: this would have called substitute.js' "received" method. +// fake.received(2); -//GOOD: we now call the "received" method we have defined in the class above. +// GOOD: we now call the "received" method we have defined in the class above. Substitute.disableFor(fake).received(1337); -//now we can assert that we received a call to the "received" method. +// now we can assert that we received a call to the "received" method. fake.received().received(1337); ``` From 6ef7b8ea8d3c7453993948bd2fd4320af8015887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Sun, 17 May 2020 15:29:05 +0200 Subject: [PATCH 08/14] require one input on substitute methods --- src/Transformations.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Transformations.ts b/src/Transformations.ts index c9df3fd..3b4af76 100644 --- a/src/Transformations.ts +++ b/src/Transformations.ts @@ -46,22 +46,24 @@ export type FunctionSubstitute = export type NoArgumentFunctionSubstitute = (() => (TReturnType & NoArgumentMockObjectMixin)) export type PropertySubstitute = (TReturnType & Partial>); +type OneArgumentRequiredFunction = (requiredInput: TArgs, ...restInputs: TArgs[]) => TReturnType; + type MockObjectPromise = TReturnType extends Promise ? { - resolves: (...args: U[]) => void; - rejects: (exception: any) => void; + resolves: OneArgumentRequiredFunction; + rejects: OneArgumentRequiredFunction; } : {} type BaseMockObjectMixin = MockObjectPromise & { - returns: (...args: TReturnType[]) => void; - throws: (exception: any) => never; + returns: OneArgumentRequiredFunction; + throws: OneArgumentRequiredFunction; } type NoArgumentMockObjectMixin = BaseMockObjectMixin & { - mimicks: (func: () => TReturnType) => void; + mimicks: OneArgumentRequiredFunction<() => TReturnType, void>; } type MockObjectMixin = BaseMockObjectMixin & { - mimicks: (func: (...args: TArguments) => TReturnType) => void; + mimicks: OneArgumentRequiredFunction<(...args: TArguments) => TReturnType, void>; } export type ObjectSubstitute = ObjectSubstituteTransformation & { From 529fadd2f1aa16916544513ce7799bb7abcb23ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Sun, 17 May 2020 15:30:29 +0200 Subject: [PATCH 09/14] refactor and renaming code --- src/Context.ts | 32 +++++++++---------- src/Substitute.ts | 17 +++++----- src/SubstituteBase.ts | 4 +-- src/Utilities.ts | 32 +++++-------------- src/states/ContextState.ts | 5 ++- src/states/InitialState.ts | 58 ++++++++++++++++++++-------------- src/states/SetPropertyState.ts | 20 ++++++------ 7 files changed, 81 insertions(+), 87 deletions(-) diff --git a/src/Context.ts b/src/Context.ts index 50ca010..9c3d415 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -1,9 +1,9 @@ import { inspect } from 'util' -import { ContextState } from "./states/ContextState"; -import { InitialState } from "./states/InitialState"; -import { HandlerKey } from "./Substitute"; -import { Type } from "./Utilities"; -import { SetPropertyState } from "./states/SetPropertyState"; +import { ContextState } from './states/ContextState'; +import { InitialState } from './states/InitialState'; +import { HandlerKey } from './Substitute'; +import { PropertyType } from './Utilities'; +import { SetPropertyState } from './states/SetPropertyState'; import { SubstituteJS as SubstituteBase, SubstituteException } from './SubstituteBase' export class Context { @@ -23,9 +23,9 @@ export class Context { this._getState = this._initialState; this._proxy = new Proxy(SubstituteBase, { - apply: (_target, _this, args) => this.apply(_target, _this, args), - set: (_target, property, value) => (this.set(_target, property, value), true), - get: (_target, property) => this._filterAndReturnProperty(_target, property, this.get) + apply: (_target, _this, args) => this.getStateApply(_target, _this, args), + set: (_target, property, value) => (this.setStateSet(_target, property, value), true), + get: (_target, property) => this._filterAndReturnProperty(_target, property, this.getStateGet) }); this._rootProxy = new Proxy(SubstituteBase, { @@ -36,11 +36,11 @@ export class Context { this._receivedProxy = new Proxy(SubstituteBase, { apply: (_target, _this, args) => this._receivedState === void 0 ? void 0 : this._receivedState.apply(this, args), - set: (_target, property, value) => (this.set(_target, property, value), true), + set: (_target, property, value) => (this.setStateSet(_target, property, value), true), get: (_target, property) => { const state = this.initialState.getPropertyStates.find(getPropertyState => getPropertyState.property === property); if (state === void 0) return this.handleNotFoundState(property); - if (!state.functionState) + if (!state.isFunctionState) state.get(this, property); this._receivedState = state; return this.receivedProxy; @@ -48,7 +48,7 @@ export class Context { }); } - private _filterAndReturnProperty(target: typeof SubstituteBase, property: PropertyKey, defaultGet: Context['get']) { + private _filterAndReturnProperty(target: typeof SubstituteBase, property: PropertyKey, getToExecute: ContextState['get']) { switch (property) { case 'constructor': case 'valueOf': @@ -68,13 +68,13 @@ export class Context { return target.prototype[Symbol.toStringTag]; default: target.prototype.lastRegisteredSubstituteJSMethodOrProperty = property.toString() - return defaultGet.bind(this)(target, property); + return getToExecute.bind(this)(target as any, property); } } private handleNotFoundState(property: PropertyKey) { if (this.initialState.hasExpectations && this.initialState.expectedCount !== null) { - this.initialState.assertCallCountMatchesExpectations([], 0, Type.property, property, []); + this.initialState.assertCallCountMatchesExpectations([], 0, PropertyType.property, property, []); return this.receivedProxy; } throw SubstituteException.forPropertyNotMocked(property); @@ -84,15 +84,15 @@ export class Context { return this.initialState.get(this, property); } - apply(_target: any, _this: any, args: any[]) { + getStateApply(_target: any, _this: any, args: any[]) { return this._getState.apply(this, args); } - set(_target: any, property: PropertyKey, value: any) { + setStateSet(_target: any, property: PropertyKey, value: any) { return this._setState.set(this, property, value); } - get(_target: any, property: PropertyKey) { + getStateGet(_target: any, property: PropertyKey) { if (property === HandlerKey) { return this; } diff --git a/src/Substitute.ts b/src/Substitute.ts index f229837..cc1ea08 100644 --- a/src/Substitute.ts +++ b/src/Substitute.ts @@ -1,6 +1,5 @@ -import { Context } from "./Context"; -import { ObjectSubstitute, OmitProxyMethods, DisabledSubstituteObject } from "./Transformations"; -import { Get } from './Utilities' +import { Context } from './Context'; +import { ObjectSubstitute, OmitProxyMethods, DisabledSubstituteObject } from './Transformations'; export const HandlerKey = Symbol(); export const AreProxiesDisabledKey = Symbol(); @@ -18,8 +17,8 @@ export class Substitute { const thisExposedProxy = thisProxy[HandlerKey]; // Context const disableProxy = (f: K): K => { - return function() { - thisProxy[AreProxiesDisabledKey] = true; // for what reason need to do this? + return function () { + thisProxy[AreProxiesDisabledKey] = true; const returnValue = f.call(thisExposedProxy, ...arguments); thisProxy[AreProxiesDisabledKey] = false; return returnValue; @@ -28,14 +27,14 @@ export class Substitute { return new Proxy(() => { }, { apply: function (_target, _this, args) { - return disableProxy(thisExposedProxy.apply)(...arguments) + return disableProxy(thisExposedProxy.getStateApply)(...arguments) }, set: function (_target, property, value) { - return disableProxy(thisExposedProxy.set)(...arguments) + return disableProxy(thisExposedProxy.setStateSet)(...arguments) }, get: function (_target, property) { - Get(thisExposedProxy._initialState, thisExposedProxy, property) - return disableProxy(thisExposedProxy.get)(...arguments) + thisExposedProxy._initialState.handleGet(thisExposedProxy, property) + return disableProxy(thisExposedProxy.getStateGet)(...arguments) } }) as any; } diff --git a/src/SubstituteBase.ts b/src/SubstituteBase.ts index 38284fc..40e1931 100644 --- a/src/SubstituteBase.ts +++ b/src/SubstituteBase.ts @@ -1,5 +1,5 @@ import { inspect } from 'util'; -import { Type, stringifyArguments, stringifyCalls, Call } from './Utilities'; +import { PropertyType, stringifyArguments, stringifyCalls, Call } from './Utilities'; export class SubstituteJS { private _lastRegisteredSubstituteJSMethodOrProperty: string @@ -52,7 +52,7 @@ export class SubstituteException extends Error { static forCallCountMissMatch( callCount: { expected: number | null, received: number }, - property: { type: Type, value: PropertyKey }, + property: { type: PropertyType, value: PropertyKey }, calls: { expectedArguments: any[], received: Call[] } ) { const message = 'Expected ' + (callCount.expected === null ? '1 or more' : callCount.expected) + diff --git a/src/Utilities.ts b/src/Utilities.ts index ddfcc3a..207c05d 100644 --- a/src/Utilities.ts +++ b/src/Utilities.ts @@ -6,7 +6,7 @@ import * as util from 'util'; export type Call = any[] // list of args -export enum Type { +export enum PropertyType { method = 'method', property = 'property' } @@ -22,8 +22,6 @@ export enum SubstituteMethods { } const seenObject = Symbol(); -export const Nothing = Symbol(); -export type Nothing = typeof Nothing export function stringifyArguments(args: any[]) { args = args.map(x => util.inspect(x)); @@ -35,7 +33,7 @@ export function areArgumentArraysEqual(a: any[], b: any[]) { return true; } - for (var i = 0; i < Math.max(b.length, a.length); i++) { + for (let i = 0; i < Math.max(b.length, a.length); i++) { if (!areArgumentsEqual(b[i], a[i])) { return false; } @@ -74,15 +72,16 @@ export function areArgumentsEqual(a: any, b: any) { return deepEqual(a, b); }; -function deepEqual(realA: any, realB: any, objectReferences: Object[] = []): boolean { +function deepEqual(realA: any, realB: any, objectReferences: object[] = []): boolean { const a = objectReferences.includes(realA) ? seenObject : realA; const b = objectReferences.includes(realB) ? seenObject : realB; const newObjectReferences = updateObjectReferences(objectReferences, a, b); if (nonNullObject(a) && nonNullObject(b)) { if (a.constructor !== b.constructor) return false; - if (Object.keys(a).length !== Object.keys(b).length) return false; - for (const key in a) { + const objectAKeys = Object.keys(a); + if (objectAKeys.length !== Object.keys(b).length) return false; + for (const key of objectAKeys) { if (!deepEqual(a[key], b[key], newObjectReferences)) return false; } return true; @@ -90,26 +89,11 @@ function deepEqual(realA: any, realB: any, objectReferences: Object[] = []): boo return a === b; } -function updateObjectReferences(objectReferences: Array, a: any, b: any) { +function updateObjectReferences(objectReferences: Array, a: any, b: any) { const tempObjectReferences = [...objectReferences, nonNullObject(a) && !objectReferences.includes(a) ? a : void 0]; return [...tempObjectReferences, nonNullObject(b) && !tempObjectReferences.includes(b) ? b : void 0]; } -function nonNullObject(value: any) { +function nonNullObject(value: any): value is { [key: string]: any } { return typeof value === 'object' && value !== null; -} - -export function Get(recorder: InitialState, context: Context, property: PropertyKey) { - const existingGetState = recorder.getPropertyStates.find(state => state.property === property); - if (existingGetState) { - context.state = existingGetState; - return context.get(void 0, property); - } - - const getState = new GetPropertyState(property); - context.state = getState; - - recorder.recordGetPropertyState(property, getState); - - return context.get(void 0, property); } \ No newline at end of file diff --git a/src/states/ContextState.ts b/src/states/ContextState.ts index 78d5da5..6448fbd 100644 --- a/src/states/ContextState.ts +++ b/src/states/ContextState.ts @@ -1,11 +1,10 @@ import { Context } from "../Context"; -import { FunctionState } from "./FunctionState"; -export type PropertyKey = string|number|symbol; +export type PropertyKey = string | number | symbol; export interface ContextState { onSwitchedTo?(context: Context): void; - apply(context: Context, args: any[], matchingFunctionStates?: FunctionState[]): any; + apply(context: Context, args: any[]): any; set(context: Context, property: PropertyKey, value: any): void; get(context: Context, property: PropertyKey): any; } \ No newline at end of file diff --git a/src/states/InitialState.ts b/src/states/InitialState.ts index 94cc0ed..847c6a4 100644 --- a/src/states/InitialState.ts +++ b/src/states/InitialState.ts @@ -1,10 +1,10 @@ -import { ContextState, PropertyKey } from "./ContextState"; -import { Context } from "../Context"; -import { GetPropertyState } from "./GetPropertyState"; -import { SetPropertyState } from "./SetPropertyState"; -import { SubstituteMethods, stringifyArguments, stringifyCalls, Call, Type, Get } from "../Utilities"; -import { AreProxiesDisabledKey } from "../Substitute"; -import { SubstituteException } from "../SubstituteBase"; +import { ContextState, PropertyKey } from './ContextState'; +import { Context } from '../Context'; +import { GetPropertyState } from './GetPropertyState'; +import { SetPropertyState } from './SetPropertyState'; +import { SubstituteMethods, Call, PropertyType } from '../Utilities'; +import { AreProxiesDisabledKey } from '../Substitute'; +import { SubstituteException } from '../SubstituteBase'; export class InitialState implements ContextState { private recordedGetPropertyStates: Map; @@ -13,29 +13,27 @@ export class InitialState implements ContextState { private _expectedCount: number | undefined | null; private _areProxiesDisabled: boolean; - public get expectedCount() { - // expected count of calls, - // being assigned with received() method call + public get expectedCount(): number | undefined | null { return this._expectedCount; } - public get hasExpectations() { + public get hasExpectations(): boolean { return this._expectedCount !== void 0; } - public get setPropertyStates() { + public get setPropertyStates(): SetPropertyState[] { return [...this.recordedSetPropertyStates]; } - public get getPropertyStates() { + public get getPropertyStates(): GetPropertyState[] { return [...this.recordedGetPropertyStates.values()]; } - public recordGetPropertyState(property: PropertyKey, getState: GetPropertyState) { + public recordGetPropertyState(property: PropertyKey, getState: GetPropertyState): void { this.recordedGetPropertyStates.set(property, getState); } - public recordSetPropertyState(setState: SetPropertyState) { + public recordSetPropertyState(setState: SetPropertyState): void { this.recordedSetPropertyStates.push(setState); } @@ -47,13 +45,13 @@ export class InitialState implements ContextState { this._expectedCount = void 0; } - assertCallCountMatchesExpectations( - receivedCalls: Call[], // list of arguments + public assertCallCountMatchesExpectations( + receivedCalls: Call[], receivedCount: number, - type: Type, // method or property + type: PropertyType, propertyValue: PropertyKey, args: any[] - ) { + ): void | never { const expectedCount = this._expectedCount; this.clearExpectations(); @@ -85,16 +83,16 @@ export class InitialState implements ContextState { return; } - const existingSetState = this.recordedSetPropertyStates.find(x => x.arguments[0] === value);; + const existingSetState = this.recordedSetPropertyStates.find(x => x.arguments[0] === value); if (existingSetState) { return existingSetState.set(context, property, value); } const setPropertyState = new SetPropertyState(property, value); - setPropertyState.set(context, property, value); + this.recordedSetPropertyStates.push(setPropertyState); context.state = setPropertyState; - this.recordedSetPropertyStates.push(setPropertyState); + return context.setStateSet(context, property, value); } get(context: Context, property: PropertyKey) { @@ -112,7 +110,7 @@ export class InitialState implements ContextState { return context.receivedProxy; }; default: - return Get(this, context, property); + return this.handleGet(context, property); } } @@ -123,4 +121,18 @@ export class InitialState implements ContextState { onSwitchedTo() { this.clearExpectations(); } + + public handleGet(context: Context, property: PropertyKey) { + const existingGetState = this.getPropertyStates.find(state => state.property === property); + if (existingGetState !== void 0) { + context.state = existingGetState; + return context.getStateGet(void 0, property); + } + + const getState = new GetPropertyState(property); + this.recordGetPropertyState(property, getState); + + context.state = getState; + return context.getStateGet(void 0, property); + } } \ No newline at end of file diff --git a/src/states/SetPropertyState.ts b/src/states/SetPropertyState.ts index 87d1502..c7e62fd 100644 --- a/src/states/SetPropertyState.ts +++ b/src/states/SetPropertyState.ts @@ -1,7 +1,7 @@ -import { ContextState, PropertyKey } from "./ContextState"; -import { Context } from "../Context"; -import { areArgumentsEqual, Type } from "../Utilities"; -import { SubstituteException } from "../SubstituteBase"; +import { ContextState, PropertyKey } from './ContextState'; +import { Context } from '../Context'; +import { areArgumentsEqual, PropertyType } from '../Utilities'; +import { SubstituteException } from '../SubstituteBase'; export class SetPropertyState implements ContextState { private _callCount: number; @@ -21,12 +21,11 @@ export class SetPropertyState implements ContextState { constructor(private _property: PropertyKey, ...args: any[]) { this._arguments = args; - this._callCount = 0; } apply(context: Context): undefined { - throw SubstituteException.generic('Calling apply of setPropertyState is not normal behaviour, something gone wrong') + throw SubstituteException.generic('Calling apply of setPropertyState is not normal behaviour, something went wrong'); } set(context: Context, property: PropertyKey, value: any) { @@ -41,11 +40,12 @@ export class SetPropertyState implements ContextState { } context.initialState.assertCallCountMatchesExpectations( - [[]], // not sure what this was supposed to do + [[]], callCount, - Type.property, + PropertyType.property, this.property, - this.arguments); + this.arguments + ); if (!hasExpectations) { this._callCount++; @@ -53,6 +53,6 @@ export class SetPropertyState implements ContextState { } get(context: Context, property: PropertyKey): undefined { - throw SubstituteException.generic('Calling get of setPropertyState is not normal behaviour, something gone wrong') + throw SubstituteException.generic('Calling get of setPropertyState is not normal behaviour, something went wrong'); } } \ No newline at end of file From f401db7a59a9df1b0edc7b6403a1b4cd71df4b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Sun, 17 May 2020 15:30:52 +0200 Subject: [PATCH 10/14] merge function and get states --- src/states/FunctionState.ts | 177 ------------------------------- src/states/GetPropertyState.ts | 188 +++++++++++++++++---------------- 2 files changed, 99 insertions(+), 266 deletions(-) delete mode 100644 src/states/FunctionState.ts diff --git a/src/states/FunctionState.ts b/src/states/FunctionState.ts deleted file mode 100644 index 69afde3..0000000 --- a/src/states/FunctionState.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { ContextState, PropertyKey } from "./ContextState"; -import { Context } from "../Context"; -import { SubstituteMethods, areArgumentArraysEqual, Call, Type } from "../Utilities"; -import { GetPropertyState } from "./GetPropertyState"; -import { SubstituteException } from "../SubstituteBase"; - -interface ReturnMock { - args: Call - returnValues: any[] | Symbol // why symbol, what - returnIndex: 0 -} -interface MimickMock { - args: Call - mimickFunction: Function -} -interface ThrowMock { - args: Call - throwFunction: any -} - -export class FunctionState implements ContextState { - private returns: ReturnMock[]; - private mimicks: MimickMock[]; - private throws: ThrowMock[]; - - private _calls: Call[]; // list of lists of arguments this was called with - private _lastArgs?: Call // bit of a hack - - public get calls(): Call[] { - return this._calls - } - - public get callCount() { - return this._calls.length; - } - - public get property() { - return this._getPropertyState.property; - } - - constructor(private _getPropertyState: GetPropertyState) { - this.returns = []; - this.mimicks = []; - this._calls = []; - this.throws = []; - } - - private getCallCount(args: Call): number { - return this._calls.filter(callArgs => areArgumentArraysEqual(callArgs, args)).length; - } - - apply(context: Context, args: any[]) { - const hasExpectations = context.initialState.hasExpectations; - this._lastArgs = args - - context.initialState.assertCallCountMatchesExpectations( - this._calls, - this.getCallCount(args), - Type.method, - this.property, - args); - - if (!hasExpectations) { - this._calls.push(args) - } - - if (!hasExpectations) { - if (this.mimicks.length > 0) { - const mimicks = this.mimicks.find(mimick => areArgumentArraysEqual(mimick.args, args)) - if (mimicks !== void 0) return mimicks.mimickFunction.apply(mimicks.mimickFunction, args); - } - - if (this.throws.length > 0) { - const possibleThrow = this.throws.find(throws => areArgumentArraysEqual(throws.args, args)) - if (possibleThrow !== void 0) throw possibleThrow.throwFunction; - } - - if (!this.returns.length) - return context.proxy; - const returns = this.returns.find(r => areArgumentArraysEqual(r.args, args)) - - if (returns) { - const returnValues = returns.returnValues as any[] - if (returnValues.length === 1) { - return returnValues[0] - } - if (returnValues.length > returns.returnIndex) { - return returnValues[returns.returnIndex++] - } - return void 0 // probably a test setup error, imho throwin is more helpful -- domasx2 - //throw Error(`${String(this._getPropertyState.property)} with ${stringifyArguments(returns.args)} called ${returns.returnIndex + 1} times, but only ${returnValues.length} return values were set up`) - } - } - return context.proxy - } - - set(context: Context, property: PropertyKey, value: any) { - } - - get(context: Context, property: PropertyKey) { - if (property === 'then') - return void 0; - - if (property === SubstituteMethods.mimicks) { - return (input: Function) => { - if (!this._lastArgs) { - throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this mimicks :/') - } - this.mimicks.push({ - args: this._lastArgs, - mimickFunction: input - }) - this._calls.pop() - - context.state = context.initialState; - } - } - - if (property === SubstituteMethods.throws) { - return (input: Error | Function) => { - if (!this._lastArgs) { - throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this throw :/') - } - this.throws.push({ - args: this._lastArgs, - throwFunction: input - }); - this._calls.pop(); - context.state = context.initialState; - } - } - - if (property === SubstituteMethods.returns - || property === SubstituteMethods.resolves - || property === SubstituteMethods.rejects - ) { - return (...returnValues: any[]) => { - if (!this._lastArgs) { - throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this return :/'); - } - const returnMock: Partial = { returnIndex: 0, args: this._lastArgs }; - const returns = returnValues.length === 0 ? [void 0] : returnValues - switch (property) { - case SubstituteMethods.returns: - returnMock.returnValues = returns; - break; - case SubstituteMethods.resolves: - returnMock.returnValues = returns.map(value => Promise.resolve(value)); - break; - case SubstituteMethods.rejects: - returnMock.returnValues = returns.map(value => Promise.reject(value)); - break; - default: - throw SubstituteException.generic( - `Expected one of the following methods: "${SubstituteMethods.returns}", "${SubstituteMethods.resolves}" or "${SubstituteMethods.rejects}"` - ); - } - this.returns.push(returnMock); - this._calls.pop() - - if (this.callCount === 0) { - // var indexOfSelf = this - // ._getPropertyState - // .recordedFunctionStates - // .indexOf(this); - // this._getPropertyState - // .recordedFunctionStates - // .splice(indexOfSelf, 1); - } - - context.state = context.initialState; - }; - } - - return context.proxy; - } -} \ No newline at end of file diff --git a/src/states/GetPropertyState.ts b/src/states/GetPropertyState.ts index 20c54c6..a5abd91 100644 --- a/src/states/GetPropertyState.ts +++ b/src/states/GetPropertyState.ts @@ -1,121 +1,131 @@ -import { ContextState, PropertyKey } from "./ContextState"; -import { Context } from "../Context"; -import { FunctionState } from "./FunctionState"; -import { Type, Nothing, SubstituteMethods } from "../Utilities"; -import { SubstituteException } from "../SubstituteBase"; +import { ContextState, PropertyKey } from './ContextState'; +import { Context } from '../Context'; +import { PropertyType, SubstituteMethods, Call, areArgumentArraysEqual } from '../Utilities'; +import { SubstituteException } from '../SubstituteBase'; -export class GetPropertyState implements ContextState { - private returns: any[] | Nothing; - private mimicks: Function | Nothing; - private throws: any; - - private _callCount: number; - private _functionState?: FunctionState; +interface SubstituteMock { + arguments: Call + mockValues: any[] + substituteType: SubstituteMethods +} - private get isFunction(): boolean { - return !!this._functionState - } +export class GetPropertyState implements ContextState { + private _mocks: SubstituteMock[]; + private _recordedCalls: Call[]; + private _isFunctionState: boolean; + private _lastArgs?: Call; - public get property() { + public get property(): PropertyKey { return this._property; } - public get callCount() { - return this._callCount; + get isFunctionState(): boolean { + return this._isFunctionState; } - public get functionState(): FunctionState | undefined { - return this._functionState + public get callCount(): number { + return this._recordedCalls.length; } constructor(private _property: PropertyKey) { - this.returns = Nothing; - this.mimicks = Nothing; - this.throws = Nothing; - this._callCount = 0; + this._mocks = []; + this._recordedCalls = []; + this._isFunctionState = false; } - apply(context: Context, args: any[]) { - this._callCount = 0; + private getCallCount(args: Call): number { + const callFilter = (recordedCall: Call): boolean => areArgumentArraysEqual(recordedCall, args); + return this._recordedCalls.filter(callFilter).length; + } - if (this.functionState) { - context.state = this.functionState - return this.functionState.apply(context, args); + private applySubstituteMethodLogic(substituteMethod: SubstituteMethods, mockValue: any, args?: Call) { + switch (substituteMethod) { + case SubstituteMethods.resolves: + return Promise.resolve(mockValue); + case SubstituteMethods.rejects: + return Promise.reject(mockValue); + case SubstituteMethods.returns: + return mockValue; + case SubstituteMethods.throws: + throw mockValue; + case SubstituteMethods.mimicks: + return mockValue.apply(mockValue, args); + default: + throw SubstituteException.generic(`Method ${substituteMethod} not implemented`) } - - var functionState = new FunctionState(this); - context.state = functionState; - this._functionState = functionState - - return context.apply(void 0, void 0, args); } - set(context: Context, property: PropertyKey, value: any) { } - - get(context: Context, property: PropertyKey) { + private processProperty(context: Context, args: any[], propertyType: PropertyType) { const hasExpectations = context.initialState.hasExpectations; - - if (property === 'then') - return void 0; - - if (this.isFunction) - return context.proxy; - - if (property === SubstituteMethods.mimicks) { - return (input: Function) => { - this.mimicks = input; - this._callCount--; - - context.state = context.initialState; + if (!hasExpectations) { + this._recordedCalls.push(args); + const foundSubstitute = this._mocks.find(mock => areArgumentArraysEqual(mock.arguments, args)); + if (foundSubstitute !== void 0) { + const mockValue = foundSubstitute.mockValues.length > 1 ? + foundSubstitute.mockValues.shift() : + foundSubstitute.mockValues[0]; + return this.applySubstituteMethodLogic(foundSubstitute.substituteType, mockValue, args); } } - if (property === SubstituteMethods.returns) { - if (this.returns !== Nothing) - throw SubstituteException.generic('The return value for the property ' + this._property.toString() + ' has already been set to ' + this.returns); - - return (...returns: any[]) => { - this.returns = returns; - this._callCount--; - - context.state = context.initialState; - }; - } + context.initialState.assertCallCountMatchesExpectations( + this._recordedCalls, + this.getCallCount(args), + propertyType, + this.property, + args + ); - if (property === SubstituteMethods.throws) { - return (callback: Function) => { - this.throws = callback; - this._callCount--; + return context.proxy; + } - context.state = context.initialState; - } + apply(context: Context, args: any[]) { + if (!this._isFunctionState) { + this._isFunctionState = true; + this._recordedCalls = []; } + this._lastArgs = args; + return this.processProperty(context, args, PropertyType.method); + } - if (!hasExpectations) { - this._callCount++; - - if (this.mimicks !== Nothing) - return this.mimicks.apply(this.mimicks); + set(context: Context, property: PropertyKey, value: any) { } - if (this.throws !== Nothing) - throw this.throws + private isSubstituteMethod(property: PropertyKey): property is SubstituteMethods { + return property === SubstituteMethods.returns || + property === SubstituteMethods.mimicks || + property === SubstituteMethods.throws || + property === SubstituteMethods.resolves || + property === SubstituteMethods.rejects; + } - if (this.returns !== Nothing) { - var returnsArray = this.returns as any[]; - if (returnsArray.length === 1) - return returnsArray[0]; + private sanitizeSubstituteMockInputs(mockInputs: Call): Call { + if (mockInputs.length === 0) return [undefined]; + return mockInputs.length > 1 ? + [...mockInputs, undefined] : + [...mockInputs]; + } - return returnsArray[this._callCount - 1]; + get(context: Context, property: PropertyKey) { + if (property === 'then') return void 0; + + if (this.isSubstituteMethod(property)) { + return (...inputs: Call) => { + const mockInputs = this.sanitizeSubstituteMockInputs(inputs); + const args = this._isFunctionState ? this._lastArgs : []; + if (args === void 0) + throw SubstituteException.generic('Eh, there\'s a bug, no args recorded :/'); + + this._mocks.push({ + arguments: args, + mockValues: mockInputs, + substituteType: property + }); + + this._recordedCalls.pop(); + context.state = context.initialState; } } - - context.initialState.assertCallCountMatchesExpectations( - [[]], // I'm not sure what this was supposed to mean - this.callCount, - Type.property, - this.property, - []); - - return context.proxy; + if (this._isFunctionState) return context.proxy; + return this.processProperty(context, [], PropertyType.property); } } \ No newline at end of file From 959cf613379836a132c4940f82e1ed4c9ad004db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Sun, 17 May 2020 15:31:15 +0200 Subject: [PATCH 11/14] remove skip on broken tests --- spec/rejects.spec.ts | 2 +- spec/resolves.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/rejects.spec.ts b/spec/rejects.spec.ts index f974987..40777d8 100644 --- a/spec/rejects.spec.ts +++ b/spec/rejects.spec.ts @@ -22,7 +22,7 @@ test('rejects a method with arguments', async t => { await t.throwsAsync(calculator.heavyOperation(0, 1, 1, 2, 4, 5, 8), { instanceOf: Error, message: 'Wrong sequence!' }); }); -test.skip('rejects a property', async t => { +test('rejects a property', async t => { const calculator = Substitute.for(); calculator.model.rejects(new Error('No model')); diff --git a/spec/resolves.spec.ts b/spec/resolves.spec.ts index b2cc00d..a3b0c73 100644 --- a/spec/resolves.spec.ts +++ b/spec/resolves.spec.ts @@ -22,7 +22,7 @@ test('resolves a method with arguments', async t => { t.is(await calculator.heavyOperation(0, 1, 1, 2, 3, 5, 8), 13); }); -test.skip('resolves a property', async t => { +test('resolves a property', async t => { const calculator = Substitute.for(); calculator.model.resolves('Casio FX-82'); From 80a537ca81a4989d4168a71c86a39a2995787b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Sun, 17 May 2020 20:26:07 +0200 Subject: [PATCH 12/14] linting and nullish coalescing --- src/states/InitialState.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/states/InitialState.ts b/src/states/InitialState.ts index 847c6a4..d7648ec 100644 --- a/src/states/InitialState.ts +++ b/src/states/InitialState.ts @@ -58,11 +58,11 @@ export class InitialState implements ContextState { if (this.doesCallCountMatchExpectations(expectedCount, receivedCount)) return; - const callCount = { expected: expectedCount, received: receivedCount } - const property = { type, value: propertyValue } - const calls = { expectedArguments: args, received: receivedCalls } + const callCount = { expected: expectedCount, received: receivedCount }; + const property = { type, value: propertyValue }; + const calls = { expectedArguments: args, received: receivedCalls }; - throw SubstituteException.forCallCountMissMatch(callCount, property, calls) + throw SubstituteException.forCallCountMissMatch(callCount, property, calls); } private doesCallCountMatchExpectations(expectedCount: number | undefined | null, actualCount: number) { @@ -101,7 +101,7 @@ export class InitialState implements ContextState { return this._areProxiesDisabled; case SubstituteMethods.received: return (count?: number) => { - this._expectedCount = count === void 0 ? null : count; + this._expectedCount = count ?? null; return context.receivedProxy; }; case SubstituteMethods.didNotReceive: From 8d6155f04e97e387fe0fac06deeff1d4c663ecfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Sun, 17 May 2020 20:26:35 +0200 Subject: [PATCH 13/14] add typescript's Omit polyfill --- src/Transformations.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Transformations.ts b/src/Transformations.ts index 3b4af76..e6a34b0 100644 --- a/src/Transformations.ts +++ b/src/Transformations.ts @@ -90,8 +90,7 @@ type ObjectSubstituteTransformation = { PropertySubstitute; } -type Omit = Pick>; +type Omit = Pick>; -// @ts-expect-error export type OmitProxyMethods = Omit; export type DisabledSubstituteObject = T extends ObjectSubstitute, infer K> ? K : never; From aec73094786dba5eab6cca2d4e9cefc7eda94cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Sun, 17 May 2020 20:30:39 +0200 Subject: [PATCH 14/14] add more tests to resolves and rejects --- spec/rejects.spec.ts | 22 ++++++++++++++++++++++ spec/resolves.spec.ts | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/spec/rejects.spec.ts b/spec/rejects.spec.ts index 40777d8..8aa1db2 100644 --- a/spec/rejects.spec.ts +++ b/spec/rejects.spec.ts @@ -22,9 +22,31 @@ test('rejects a method with arguments', async t => { await t.throwsAsync(calculator.heavyOperation(0, 1, 1, 2, 4, 5, 8), { instanceOf: Error, message: 'Wrong sequence!' }); }); +test('rejects different values in the specified order on a method', async t => { + const calculator = Substitute.for(); + calculator.heavyOperation(Arg.any('number')).rejects(new Error('Wrong!'), new Error('Wrong again!')); + + await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong!' }); + await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong again!' }); + await calculator.heavyOperation(0) + .then(() => t.fail('Promise.catch should have been executed')) + .catch(error => t.is(error, void 0)); +}); + test('rejects a property', async t => { const calculator = Substitute.for(); calculator.model.rejects(new Error('No model')); await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' }); }); + +test('rejects different values in the specified order on a property', async t => { + const calculator = Substitute.for(); + calculator.model.rejects(new Error('No model'), new Error('I said "no model"')); + + await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' }); + await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'I said "no model"' }); + await calculator.model + .then(() => t.fail('Promise.catch should have been executed')) + .catch(error => t.is(error, void 0)); +}); diff --git a/spec/resolves.spec.ts b/spec/resolves.spec.ts index a3b0c73..aaf5bb7 100644 --- a/spec/resolves.spec.ts +++ b/spec/resolves.spec.ts @@ -22,9 +22,28 @@ test('resolves a method with arguments', async t => { t.is(await calculator.heavyOperation(0, 1, 1, 2, 3, 5, 8), 13); }); +test('resolves different values in the specified order on a method', async t => { + const calculator = Substitute.for(); + calculator.heavyOperation(Arg.any('number')).resolves(1, 2, 3); + + t.is(await calculator.heavyOperation(0), 1); + t.is(await calculator.heavyOperation(0), 2); + t.is(await calculator.heavyOperation(0), 3); + t.is(await calculator.heavyOperation(0), void 0); +}); + test('resolves a property', async t => { const calculator = Substitute.for(); calculator.model.resolves('Casio FX-82'); t.is(await calculator.model, 'Casio FX-82'); }); + +test('resolves different values in the specified order on a property', async t => { + const calculator = Substitute.for(); + calculator.model.resolves('Casio FX-82', 'TI-84 Plus'); + + t.is(await calculator.model, 'Casio FX-82'); + t.is(await calculator.model, 'TI-84 Plus'); + t.is(await calculator.model, void 0); +});