Skip to content

Commit

Permalink
Mixed mocking of properties and methods
Browse files Browse the repository at this point in the history
  • Loading branch information
johanblumenberg committed May 22, 2018
1 parent b6f4c70 commit 4c6dfab
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 50 deletions.
14 changes: 7 additions & 7 deletions src/MethodStubVerificator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,32 @@ export class MethodStubVerificator<T> {
}

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).`);
}
}

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).`);
}
}

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).`);
}
}

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}`;
Expand All @@ -73,8 +73,8 @@ export class MethodStubVerificator<T> {
}

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}`;
Expand Down
2 changes: 1 addition & 1 deletion src/MethodToStub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export class MethodToStub {
constructor(public methodStubCollection: MethodStubCollection,
public matchers: Matcher[],
public mocker: Mocker,
public name: string) {
public methodName: string) {
}
}
83 changes: 64 additions & 19 deletions src/Mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,32 +46,30 @@ export class Mocker {
return new Proxy(this.mock, this.createCatchAllHandlerForRemainingPropertiesWithoutGetters("expectation"));
}

public createCatchAllHandlerForRemainingPropertiesWithoutGetters(origin: "instance" | "expectation"): any {
public createCatchAllHandlerForRemainingPropertiesWithoutGetters(origin: "instance" | "expectation"): any {
return {
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];
},
};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
});

let 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);
};

let 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;
Expand Down
6 changes: 3 additions & 3 deletions src/ts-mockito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ export function imock<T>(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<T>(method: T): MethodStubVerificator<T> {
Expand Down Expand Up @@ -78,7 +78,7 @@ export function capture<T0>(method: (a: T0) => any): ArgCaptor1<T0>;
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.");
Expand Down
2 changes: 1 addition & 1 deletion src/utils/MethodCallToStringConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})" `;
}
}
5 changes: 4 additions & 1 deletion test/mocking.properties.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
56 changes: 38 additions & 18 deletions test/mocking.types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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");
});

Expand All @@ -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");
});

Expand All @@ -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 {
Expand All @@ -313,11 +334,8 @@ interface SampleInterface {
dependency: Bar;

sampleMethod(): number;
}

interface SamplePropertyInterface {
foo: string;
bar: number;
sampleProperty: string;
}

class SampleInterfaceImplementation implements SampleInterface {
Expand All @@ -326,6 +344,8 @@ class SampleInterfaceImplementation implements SampleInterface {
public sampleMethod(): number {
return 999;
}

sampleProperty: "999";
}

class SampleGeneric<T> {
Expand Down

0 comments on commit 4c6dfab

Please sign in to comment.