diff --git a/src/MethodStubVerificator.ts b/src/MethodStubVerificator.ts index 6bfceb2..cff0e70 100644 --- a/src/MethodStubVerificator.ts +++ b/src/MethodStubVerificator.ts @@ -29,7 +29,7 @@ export class MethodStubVerificator { } public times(value: number): void { - const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers); + const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.methodName, this.methodToVerify.matchers); if (value !== allMatchingActions.length) { const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); throw new Error(`Expected "${methodToVerifyAsString}to be called ${value} time(s). But has been called ${allMatchingActions.length} time(s).`); @@ -37,7 +37,7 @@ export class MethodStubVerificator { } public atLeast(value: number): void { - const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers); + const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.methodName, this.methodToVerify.matchers); if (value > allMatchingActions.length) { const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); throw new Error(`Expected "${methodToVerifyAsString}to be called at least ${value} time(s). But has been called ${allMatchingActions.length} time(s).`); @@ -45,7 +45,7 @@ export class MethodStubVerificator { } public atMost(value: number): void { - const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers); + const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.methodName, this.methodToVerify.matchers); if (value < allMatchingActions.length) { const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); throw new Error(`Expected "${methodToVerifyAsString}to be called at least ${value} time(s). But has been called ${allMatchingActions.length} time(s).`); @@ -53,8 +53,8 @@ export class MethodStubVerificator { } public calledBefore(method: any): void { - const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.name, this.methodToVerify.matchers); - const secondMethodAction = method.mocker.getFirstMatchingAction(method.name, method.matchers); + const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.methodName, this.methodToVerify.matchers); + const secondMethodAction = method.mocker.getFirstMatchingAction(method.methodName, method.matchers); const mainMethodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); const secondMethodAsString = this.methodCallToStringConverter.convert(method); const errorBeginning = `Expected "${mainMethodToVerifyAsString} to be called before ${secondMethodAsString}`; @@ -73,8 +73,8 @@ export class MethodStubVerificator { } public calledAfter(method: any): void { - const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.name, this.methodToVerify.matchers); - const secondMethodAction = method.mocker.getFirstMatchingAction(method.name, method.matchers); + const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.methodName , this.methodToVerify.matchers); + const secondMethodAction = method.mocker.getFirstMatchingAction(method.methodName, method.matchers); const mainMethodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); const secondMethodAsString = this.methodCallToStringConverter.convert(method); const errorBeginning = `Expected "${mainMethodToVerifyAsString}to be called after ${secondMethodAsString}`; diff --git a/src/MethodToStub.ts b/src/MethodToStub.ts index 845fbd8..7751ed8 100644 --- a/src/MethodToStub.ts +++ b/src/MethodToStub.ts @@ -6,6 +6,6 @@ export class MethodToStub { constructor(public methodStubCollection: MethodStubCollection, public matchers: Matcher[], public mocker: Mocker, - public name: string) { + public methodName: string) { } } diff --git a/src/Mock.ts b/src/Mock.ts index a35902a..fdb6651 100644 --- a/src/Mock.ts +++ b/src/Mock.ts @@ -51,27 +51,25 @@ export class Mocker { get: (target: any, name: PropertyKey) => { const hasMethodStub = name in target; if (!hasMethodStub) { - if (this.mock.__policy === MockPropertyPolicy.StubAsMethod) { - if (origin !== "instance" || name !== "then") { - // Don't make this mock object instance look like a Promise instance by mistake, if someone is checking - this.createMethodStub(name.toString()); - this.createInstanceActionListener(name.toString(), {}); - } - } else if (this.mock.__policy === MockPropertyPolicy.StubAsProperty) { - this.createPropertyStub(name.toString()); - this.createInstancePropertyDescriptorListener(name.toString(), {}, this.clazz.prototype); - } else if (this.mock.__policy === MockPropertyPolicy.Throw) { - if (origin === "instance") { - throw new Error(`Trying to read property ${name.toString()} from a mock object, which was not expected.`); - } else { - // TODO: Assuming it is a property, not a function. Fix this... + if (origin === "instance") { + if (this.mock.__policy === MockPropertyPolicy.StubAsMethod) { + if (name !== "then") { + // Don't make this mock object instance look like a Promise instance by mistake, if someone is checking + this.createMethodStub(name.toString()); + this.createInstanceActionListener(name.toString(), {}); + } + } else if (this.mock.__policy === MockPropertyPolicy.StubAsProperty) { this.createPropertyStub(name.toString()); this.createInstancePropertyDescriptorListener(name.toString(), {}, this.clazz.prototype); + } else if (this.mock.__policy === MockPropertyPolicy.Throw) { + throw new Error(`Trying to read property ${name.toString()} from a mock object, which was not expected.`); + } else { + throw new Error("Invalid MockPolicy value"); } - } else { - throw new Error("Invalid MockPolicy value"); + } else if (origin === "expectation") { + this.createMixedStub(name.toString()); } - } + } return target[name]; }, }; @@ -112,7 +110,6 @@ export class Mocker { if (descriptor.get) { this.createPropertyStub(name); this.createInstancePropertyDescriptorListener(name, descriptor, obj); - this.createInstanceActionListener(name, obj); } else if (typeof descriptor.value === "function") { this.createMethodStub(name); this.createInstanceActionListener(name, obj); @@ -178,6 +175,54 @@ export class Mocker { }); } + private createMixedStub(key: string): void { + if (this.mock.hasOwnProperty(key)) { + return; + } + + // Assume it is a property stub, until proven otherwise + let isProperty = true; + + Object.defineProperty(this.instance, key, { + get: () => { + if (isProperty) { + return this.createActionListener(key)(); + } else { + return this.createActionListener(key); + } + }, + }); + + const methodMock = (...args) => { + isProperty = false; + + const matchers: Matcher[] = []; + + for (const arg of args) { + if (!(arg instanceof Matcher)) { + matchers.push(strictEqual(arg)); + } else { + matchers.push(arg); + } + } + + return new MethodToStub(this.methodStubCollections[key], matchers, this, key); + }; + + const propertyMock = () => { + if (!this.methodStubCollections[key]) { + this.methodStubCollections[key] = new MethodStubCollection(); + } + + // Return a mix of a method stub and a property invocation, which works as both + return Object.assign(methodMock, new MethodToStub(this.methodStubCollections[key], [], this, key)); + }; + + Object.defineProperty(this.mock, key, { + get: propertyMock, + }); + } + private createPropertyStub(key: string): void { if (this.mock.hasOwnProperty(key)) { return; diff --git a/src/ts-mockito.ts b/src/ts-mockito.ts index 9d8236b..b00dea1 100644 --- a/src/ts-mockito.ts +++ b/src/ts-mockito.ts @@ -46,8 +46,8 @@ export function imock(policy: MockPropertyPolicy = MockPropertyPolicy.StubAsM if (typeof Proxy === "undefined") { throw new Error("Mocking of interfaces requires support for Proxy objects"); } - const tsmockitoMocker = mockedValue.__tsmockitoMocker; - return new Proxy(mockedValue, tsmockitoMocker.createCatchAllHandlerForRemainingPropertiesWithoutGetters()); + const tsmockitoMocker = mockedValue.__tsmockitoMocker as Mocker; + return new Proxy(mockedValue, tsmockitoMocker.createCatchAllHandlerForRemainingPropertiesWithoutGetters("expectation")); } export function verify(method: T): MethodStubVerificator { @@ -78,7 +78,7 @@ export function capture(method: (a: T0) => any): ArgCaptor1; export function capture(method: (...args: any[]) => any): ArgCaptor { const methodStub: MethodToStub = method(); if (methodStub instanceof MethodToStub) { - const actions = methodStub.mocker.getActionsByName(methodStub.name); + const actions = methodStub.mocker.getActionsByName(methodStub.methodName); return new ArgCaptor(actions); } else { throw Error("Cannot capture from not mocked object."); diff --git a/src/utils/MethodCallToStringConverter.ts b/src/utils/MethodCallToStringConverter.ts index a9c08d6..01416b8 100644 --- a/src/utils/MethodCallToStringConverter.ts +++ b/src/utils/MethodCallToStringConverter.ts @@ -4,6 +4,6 @@ import {MethodToStub} from "../MethodToStub"; export class MethodCallToStringConverter { public convert(method: MethodToStub): string { const stringifiedMatchers = method.matchers.map((matcher: Matcher) => matcher.toString()).join(", "); - return `${method.name}(${stringifiedMatchers})" `; + return `${method.methodName}(${stringifiedMatchers})" `; } } diff --git a/test/mocking.properties.spec.ts b/test/mocking.properties.spec.ts index f00e2e8..5c9b6f1 100644 --- a/test/mocking.properties.spec.ts +++ b/test/mocking.properties.spec.ts @@ -18,7 +18,10 @@ describe("mocking", () => { when(mockedFoo.sampleNumber).thenReturn(42); // then - expect((mockedFoo.sampleNumber as any) instanceof MethodToStub).toBe(true); + expect((mockedFoo.sampleNumber as any).methodStubCollection).toBeDefined(); + expect((mockedFoo.sampleNumber as any).matchers).toBeDefined(); + expect((mockedFoo.sampleNumber as any).mocker).toBeDefined(); + expect((mockedFoo.sampleNumber as any).methodName).toBeDefined(); }); it("does create own property descriptors on instance", () => { diff --git a/test/mocking.types.spec.ts b/test/mocking.types.spec.ts index 913ca09..5880953 100644 --- a/test/mocking.types.spec.ts +++ b/test/mocking.types.spec.ts @@ -100,7 +100,7 @@ describe("mocking", () => { const result = foo.getGenericTypedValue(); // then - expect(expectedResult).toEqual(result); + expect(result).toEqual(expectedResult); }); it("does create own property descriptors on instance", () => { @@ -223,21 +223,21 @@ describe("mocking", () => { }); describe("mock an interface with properties", () => { - let mockedFoo: SamplePropertyInterface; - let foo: SamplePropertyInterface; + let mockedFoo: SampleInterface; + let foo: SampleInterface; if (typeof Proxy !== "undefined") { it("can setup call actions", () => { // given mockedFoo = imock(MockPropertyPolicy.StubAsProperty); foo = instance(mockedFoo); - when(mockedFoo.foo).thenReturn("value"); + when(mockedFoo.sampleProperty).thenReturn("value"); // when - const result = foo.foo; + const result = foo.sampleProperty; // then - verify(mockedFoo.foo).called(); + verify(mockedFoo.sampleProperty).called(); expect(result).toBe("value"); }); @@ -247,31 +247,31 @@ describe("mocking", () => { foo = instance(mockedFoo); // when - const result = foo.foo; + const result = foo.sampleProperty; // then - verify(mockedFoo.foo).called(); + verify(mockedFoo.sampleProperty).called(); expect(result).toBe(null); }); } }); describe("mock an interface with default policy to throw", () => { - let mockedFoo: SamplePropertyInterface; - let foo: SamplePropertyInterface; + let mockedFoo: SampleInterface; + let foo: SampleInterface; if (typeof Proxy !== "undefined") { it("can setup call actions", () => { // given mockedFoo = imock(MockPropertyPolicy.Throw); foo = instance(mockedFoo); - when(mockedFoo.foo).thenReturn("value"); + when(mockedFoo.sampleProperty).thenReturn("value"); // when - const result = foo.foo; + const result = foo.sampleProperty; // then - verify(mockedFoo.foo).called(); + verify(mockedFoo.sampleProperty).called(); expect(result).toBe("value"); }); @@ -281,12 +281,33 @@ describe("mocking", () => { foo = instance(mockedFoo); // when - expect(() => foo.foo).toThrow(); + expect(() => foo.sampleProperty).toThrow(); // then }); } }); + + describe("mock an interface with both properties and methods", () => { + let mockedFoo: SampleInterface; + let foo: SampleInterface; + + if (typeof Proxy !== "undefined") { + it("can setup call actions on methods", () => { + // given + mockedFoo = imock(MockPropertyPolicy.StubAsProperty); + foo = instance(mockedFoo); + when(mockedFoo.sampleMethod()).thenReturn(5); + + // when + const result = foo.sampleMethod(); + + // then + verify(mockedFoo.sampleMethod()).called(); + expect(result).toBe(5); + }); + } + }); }); abstract class SampleAbstractClass { @@ -312,17 +333,16 @@ abstract class SampleAbstractClass { interface SampleInterface { dependency: Bar; - sampleMethod(): number; -} + sampleProperty: string; -interface SamplePropertyInterface { - foo: string; - bar: number; + sampleMethod(): number; } class SampleInterfaceImplementation implements SampleInterface { public dependency: Bar; + public sampleProperty: "999"; + public sampleMethod(): number { return 999; }