Skip to content

Commit 3cf15e1

Browse files
Mixed mocking of properties and methods
1 parent e3fa96f commit 3cf15e1

7 files changed

+118
-50
lines changed

src/MethodStubVerificator.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,32 @@ export class MethodStubVerificator<T> {
2929
}
3030

3131
public times(value: number): void {
32-
const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers);
32+
const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.methodName, this.methodToVerify.matchers);
3333
if (value !== allMatchingActions.length) {
3434
const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify);
3535
throw new Error(`Expected "${methodToVerifyAsString}to be called ${value} time(s). But has been called ${allMatchingActions.length} time(s).`);
3636
}
3737
}
3838

3939
public atLeast(value: number): void {
40-
const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers);
40+
const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.methodName, this.methodToVerify.matchers);
4141
if (value > allMatchingActions.length) {
4242
const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify);
4343
throw new Error(`Expected "${methodToVerifyAsString}to be called at least ${value} time(s). But has been called ${allMatchingActions.length} time(s).`);
4444
}
4545
}
4646

4747
public atMost(value: number): void {
48-
const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers);
48+
const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.methodName, this.methodToVerify.matchers);
4949
if (value < allMatchingActions.length) {
5050
const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify);
5151
throw new Error(`Expected "${methodToVerifyAsString}to be called at least ${value} time(s). But has been called ${allMatchingActions.length} time(s).`);
5252
}
5353
}
5454

5555
public calledBefore(method: any): void {
56-
const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.name, this.methodToVerify.matchers);
57-
const secondMethodAction = method.mocker.getFirstMatchingAction(method.name, method.matchers);
56+
const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.methodName, this.methodToVerify.matchers);
57+
const secondMethodAction = method.mocker.getFirstMatchingAction(method.methodName, method.matchers);
5858
const mainMethodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify);
5959
const secondMethodAsString = this.methodCallToStringConverter.convert(method);
6060
const errorBeginning = `Expected "${mainMethodToVerifyAsString} to be called before ${secondMethodAsString}`;
@@ -73,8 +73,8 @@ export class MethodStubVerificator<T> {
7373
}
7474

7575
public calledAfter(method: any): void {
76-
const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.name, this.methodToVerify.matchers);
77-
const secondMethodAction = method.mocker.getFirstMatchingAction(method.name, method.matchers);
76+
const firstMethodAction = this.methodToVerify.mocker.getFirstMatchingAction(this.methodToVerify.methodName , this.methodToVerify.matchers);
77+
const secondMethodAction = method.mocker.getFirstMatchingAction(method.methodName, method.matchers);
7878
const mainMethodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify);
7979
const secondMethodAsString = this.methodCallToStringConverter.convert(method);
8080
const errorBeginning = `Expected "${mainMethodToVerifyAsString}to be called after ${secondMethodAsString}`;

src/MethodToStub.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export class MethodToStub {
66
constructor(public methodStubCollection: MethodStubCollection,
77
public matchers: Matcher[],
88
public mocker: Mocker,
9-
public name: string) {
9+
public methodName: string) {
1010
}
1111
}

src/Mock.ts

+63-18
Original file line numberDiff line numberDiff line change
@@ -51,27 +51,25 @@ export class Mocker {
5151
get: (target: any, name: PropertyKey) => {
5252
const hasMethodStub = name in target;
5353
if (!hasMethodStub) {
54-
if (this.mock.__policy === MockPropertyPolicy.StubAsMethod) {
55-
if (origin !== "instance" || name !== "then") {
56-
// Don't make this mock object instance look like a Promise instance by mistake, if someone is checking
57-
this.createMethodStub(name.toString());
58-
this.createInstanceActionListener(name.toString(), {});
59-
}
60-
} else if (this.mock.__policy === MockPropertyPolicy.StubAsProperty) {
61-
this.createPropertyStub(name.toString());
62-
this.createInstancePropertyDescriptorListener(name.toString(), {}, this.clazz.prototype);
63-
} else if (this.mock.__policy === MockPropertyPolicy.Throw) {
64-
if (origin === "instance") {
65-
throw new Error(`Trying to read property ${name.toString()} from a mock object, which was not expected.`);
66-
} else {
67-
// TODO: Assuming it is a property, not a function. Fix this...
54+
if (origin === "instance") {
55+
if (this.mock.__policy === MockPropertyPolicy.StubAsMethod) {
56+
if (name !== "then") {
57+
// Don't make this mock object instance look like a Promise instance by mistake, if someone is checking
58+
this.createMethodStub(name.toString());
59+
this.createInstanceActionListener(name.toString(), {});
60+
}
61+
} else if (this.mock.__policy === MockPropertyPolicy.StubAsProperty) {
6862
this.createPropertyStub(name.toString());
6963
this.createInstancePropertyDescriptorListener(name.toString(), {}, this.clazz.prototype);
64+
} else if (this.mock.__policy === MockPropertyPolicy.Throw) {
65+
throw new Error(`Trying to read property ${name.toString()} from a mock object, which was not expected.`);
66+
} else {
67+
throw new Error("Invalid MockPolicy value");
7068
}
71-
} else {
72-
throw new Error("Invalid MockPolicy value");
69+
} else if (origin === "expectation") {
70+
this.createMixedStub(name.toString());
7371
}
74-
}
72+
}
7573
return target[name];
7674
},
7775
};
@@ -112,7 +110,6 @@ export class Mocker {
112110
if (descriptor.get) {
113111
this.createPropertyStub(name);
114112
this.createInstancePropertyDescriptorListener(name, descriptor, obj);
115-
this.createInstanceActionListener(name, obj);
116113
} else if (typeof descriptor.value === "function") {
117114
this.createMethodStub(name);
118115
this.createInstanceActionListener(name, obj);
@@ -178,6 +175,54 @@ export class Mocker {
178175
});
179176
}
180177

178+
private createMixedStub(key: string): void {
179+
if (this.mock.hasOwnProperty(key)) {
180+
return;
181+
}
182+
183+
// Assume it is a property stub, until proven otherwise
184+
let isProperty = true;
185+
186+
Object.defineProperty(this.instance, key, {
187+
get: () => {
188+
if (isProperty) {
189+
return this.createActionListener(key)();
190+
} else {
191+
return this.createActionListener(key);
192+
}
193+
},
194+
});
195+
196+
const methodMock = (...args) => {
197+
isProperty = false;
198+
199+
const matchers: Matcher[] = [];
200+
201+
for (const arg of args) {
202+
if (!(arg instanceof Matcher)) {
203+
matchers.push(strictEqual(arg));
204+
} else {
205+
matchers.push(arg);
206+
}
207+
}
208+
209+
return new MethodToStub(this.methodStubCollections[key], matchers, this, key);
210+
};
211+
212+
const propertyMock = () => {
213+
if (!this.methodStubCollections[key]) {
214+
this.methodStubCollections[key] = new MethodStubCollection();
215+
}
216+
217+
// Return a mix of a method stub and a property invocation, which works as both
218+
return Object.assign(methodMock, new MethodToStub(this.methodStubCollections[key], [], this, key));
219+
};
220+
221+
Object.defineProperty(this.mock, key, {
222+
get: propertyMock,
223+
});
224+
}
225+
181226
private createPropertyStub(key: string): void {
182227
if (this.mock.hasOwnProperty(key)) {
183228
return;

src/ts-mockito.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export function imock<T>(policy: MockPropertyPolicy = MockPropertyPolicy.StubAsM
4646
if (typeof Proxy === "undefined") {
4747
throw new Error("Mocking of interfaces requires support for Proxy objects");
4848
}
49-
const tsmockitoMocker = mockedValue.__tsmockitoMocker;
50-
return new Proxy(mockedValue, tsmockitoMocker.createCatchAllHandlerForRemainingPropertiesWithoutGetters());
49+
const tsmockitoMocker = mockedValue.__tsmockitoMocker as Mocker;
50+
return new Proxy(mockedValue, tsmockitoMocker.createCatchAllHandlerForRemainingPropertiesWithoutGetters("expectation"));
5151
}
5252

5353
export function verify<T>(method: T): MethodStubVerificator<T> {
@@ -78,7 +78,7 @@ export function capture<T0>(method: (a: T0) => any): ArgCaptor1<T0>;
7878
export function capture(method: (...args: any[]) => any): ArgCaptor {
7979
const methodStub: MethodToStub = method();
8080
if (methodStub instanceof MethodToStub) {
81-
const actions = methodStub.mocker.getActionsByName(methodStub.name);
81+
const actions = methodStub.mocker.getActionsByName(methodStub.methodName);
8282
return new ArgCaptor(actions);
8383
} else {
8484
throw Error("Cannot capture from not mocked object.");

src/utils/MethodCallToStringConverter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import {MethodToStub} from "../MethodToStub";
44
export class MethodCallToStringConverter {
55
public convert(method: MethodToStub): string {
66
const stringifiedMatchers = method.matchers.map((matcher: Matcher) => matcher.toString()).join(", ");
7-
return `${method.name}(${stringifiedMatchers})" `;
7+
return `${method.methodName}(${stringifiedMatchers})" `;
88
}
99
}

test/mocking.properties.spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ describe("mocking", () => {
1818
when(mockedFoo.sampleNumber).thenReturn(42);
1919

2020
// then
21-
expect((mockedFoo.sampleNumber as any) instanceof MethodToStub).toBe(true);
21+
expect((mockedFoo.sampleNumber as any).methodStubCollection).toBeDefined();
22+
expect((mockedFoo.sampleNumber as any).matchers).toBeDefined();
23+
expect((mockedFoo.sampleNumber as any).mocker).toBeDefined();
24+
expect((mockedFoo.sampleNumber as any).methodName).toBeDefined();
2225
});
2326

2427
it("does create own property descriptors on instance", () => {

test/mocking.types.spec.ts

+39-19
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ describe("mocking", () => {
100100
const result = foo.getGenericTypedValue();
101101

102102
// then
103-
expect(expectedResult).toEqual(result);
103+
expect(result).toEqual(expectedResult);
104104
});
105105

106106
it("does create own property descriptors on instance", () => {
@@ -223,21 +223,21 @@ describe("mocking", () => {
223223
});
224224

225225
describe("mock an interface with properties", () => {
226-
let mockedFoo: SamplePropertyInterface;
227-
let foo: SamplePropertyInterface;
226+
let mockedFoo: SampleInterface;
227+
let foo: SampleInterface;
228228

229229
if (typeof Proxy !== "undefined") {
230230
it("can setup call actions", () => {
231231
// given
232232
mockedFoo = imock(MockPropertyPolicy.StubAsProperty);
233233
foo = instance(mockedFoo);
234-
when(mockedFoo.foo).thenReturn("value");
234+
when(mockedFoo.sampleProperty).thenReturn("value");
235235

236236
// when
237-
const result = foo.foo;
237+
const result = foo.sampleProperty;
238238

239239
// then
240-
verify(mockedFoo.foo).called();
240+
verify(mockedFoo.sampleProperty).called();
241241
expect(result).toBe("value");
242242
});
243243

@@ -247,31 +247,31 @@ describe("mocking", () => {
247247
foo = instance(mockedFoo);
248248

249249
// when
250-
const result = foo.foo;
250+
const result = foo.sampleProperty;
251251

252252
// then
253-
verify(mockedFoo.foo).called();
253+
verify(mockedFoo.sampleProperty).called();
254254
expect(result).toBe(null);
255255
});
256256
}
257257
});
258258

259259
describe("mock an interface with default policy to throw", () => {
260-
let mockedFoo: SamplePropertyInterface;
261-
let foo: SamplePropertyInterface;
260+
let mockedFoo: SampleInterface;
261+
let foo: SampleInterface;
262262

263263
if (typeof Proxy !== "undefined") {
264264
it("can setup call actions", () => {
265265
// given
266266
mockedFoo = imock(MockPropertyPolicy.Throw);
267267
foo = instance(mockedFoo);
268-
when(mockedFoo.foo).thenReturn("value");
268+
when(mockedFoo.sampleProperty).thenReturn("value");
269269

270270
// when
271-
const result = foo.foo;
271+
const result = foo.sampleProperty;
272272

273273
// then
274-
verify(mockedFoo.foo).called();
274+
verify(mockedFoo.sampleProperty).called();
275275
expect(result).toBe("value");
276276
});
277277

@@ -281,12 +281,33 @@ describe("mocking", () => {
281281
foo = instance(mockedFoo);
282282

283283
// when
284-
expect(() => foo.foo).toThrow();
284+
expect(() => foo.sampleProperty).toThrow();
285285

286286
// then
287287
});
288288
}
289289
});
290+
291+
describe("mock an interface with both properties and methods", () => {
292+
let mockedFoo: SampleInterface;
293+
let foo: SampleInterface;
294+
295+
if (typeof Proxy !== "undefined") {
296+
it("can setup call actions on methods", () => {
297+
// given
298+
mockedFoo = imock(MockPropertyPolicy.StubAsProperty);
299+
foo = instance(mockedFoo);
300+
when(mockedFoo.sampleMethod()).thenReturn(5);
301+
302+
// when
303+
const result = foo.sampleMethod();
304+
305+
// then
306+
verify(mockedFoo.sampleMethod()).called();
307+
expect(result).toBe(5);
308+
});
309+
}
310+
});
290311
});
291312

292313
abstract class SampleAbstractClass {
@@ -312,17 +333,16 @@ abstract class SampleAbstractClass {
312333
interface SampleInterface {
313334
dependency: Bar;
314335

315-
sampleMethod(): number;
316-
}
336+
sampleProperty: string;
317337

318-
interface SamplePropertyInterface {
319-
foo: string;
320-
bar: number;
338+
sampleMethod(): number;
321339
}
322340

323341
class SampleInterfaceImplementation implements SampleInterface {
324342
public dependency: Bar;
325343

344+
public sampleProperty: "999";
345+
326346
public sampleMethod(): number {
327347
return 999;
328348
}

0 commit comments

Comments
 (0)