Skip to content

Commit

Permalink
Add warning when forgetting to get instance of a mock
Browse files Browse the repository at this point in the history
  • Loading branch information
johanblumenberg committed Jun 21, 2019
1 parent 6a4bccb commit e2b52a7
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/MethodStubSetter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class MethodStubSetter<T, ResolveType = void, RejectType = void> {
private groupIndex: number;

constructor(private methodToStub: MethodToStub) {
methodToStub.watcher.invoked();
this.groupIndex = ++MethodStubSetter.globalGroupIndex;
}

Expand Down
6 changes: 5 additions & 1 deletion src/MethodStubVerificator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class MethodStubVerificator<T> {
private methodCallToStringConverter: MethodCallToStringConverter = new MethodCallToStringConverter();

constructor(private methodToVerify: MethodToStub) {

methodToVerify.watcher.invoked();
}

public called(): void {
Expand Down Expand Up @@ -56,6 +56,8 @@ export class MethodStubVerificator<T> {
}

public calledBefore(method: any): void {
method.watcher.invoked();

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);
Expand All @@ -76,6 +78,8 @@ export class MethodStubVerificator<T> {
}

public calledAfter(method: any): void {
method.watcher.invoked();

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);
Expand Down
31 changes: 27 additions & 4 deletions src/MethodToStub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,33 @@ import {Matcher} from "./matcher/type/Matcher";
import {MethodStubCollection} from "./MethodStubCollection";
import {Mocker} from "./Mock";

class Watcher {
private _invoked = false;

constructor(private _err: Error) {
setTimeout(this.nextTick, 0);
}

public invoked() {
this._invoked = true;
}

private nextTick = () => {
if (!this._invoked) {
throw this._err;
}
};
}

export class MethodToStub {
constructor(public methodStubCollection: MethodStubCollection,
public matchers: Matcher[],
public mocker: Mocker,
public methodName: string) {
public watcher: Watcher;

constructor(
public methodStubCollection: MethodStubCollection,
public matchers: Matcher[],
public mocker: Mocker,
public methodName: string)
{
this.watcher = new Watcher(new Error(`Unmatched call to ${methodName} on a mock object, did you mean to use instance()?`));
}
}
26 changes: 9 additions & 17 deletions src/Mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,29 +197,21 @@ export class Mocker {
},
});

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

const methodToMock = new MethodToStub(this.methodStubCollections[key], [], this, key);

const methodMock = (...args) => {
isProperty = false;
methodToMock.matchers = args.map(arg => (arg instanceof Matcher) ? arg : strictEqual(arg));
return methodToMock;
};

// 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));
return Object.assign(methodMock, methodToMock);
};

Object.defineProperty(this.mock, key, {
Expand Down
7 changes: 7 additions & 0 deletions src/ts-mockito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ 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) {
methodStub.watcher.invoked();

const actions = methodStub.mocker.getActionsByName(methodStub.methodName);
return new ArgCaptor(actions);
} else {
Expand Down Expand Up @@ -173,6 +175,10 @@ export function defer<T>(): Deferred<T> {
return Object.assign(d, { resolve, reject });
}

export function nextTick(): Promise<void> {
return new Promise(resolve => setTimeout(resolve, 0));
}

// Export default object with all members (ember-browserify doesn't support named exports).
export default {
spy,
Expand Down Expand Up @@ -200,4 +206,5 @@ export default {
objectContaining,
MockPropertyPolicy,
defer,
nextTick,
};
2 changes: 2 additions & 0 deletions src/utils/MethodCallToStringConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {MethodToStub} from "../MethodToStub";

export class MethodCallToStringConverter {
public convert(method: MethodToStub): string {
method.watcher.invoked();

const stringifiedMatchers = method.matchers.map((matcher: Matcher) => matcher.toString()).join(", ");
return `${method.methodName}(${stringifiedMatchers})" `;
}
Expand Down
16 changes: 15 additions & 1 deletion test/instance.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {instance, mock} from "../src/ts-mockito";
import {instance, mock, nextTick} from "../src/ts-mockito";
import {Foo} from "./utils/Foo";

describe("instance", () => {
Expand All @@ -17,4 +17,18 @@ describe("instance", () => {
expect(firstFooInstance).toBe(secondFooInstance);
});
});

xdescribe("forgetting to get instance of mock", () => {
let mockedFoo: Foo;

it("throws an exception if not taking instance before calling a method", async () => {
// given
mockedFoo = mock(Foo);

mockedFoo.getBar();
await nextTick();

// Expected to fail with "Unmatched call to getBar, did you forget to use instance()?"
});
});
});
12 changes: 10 additions & 2 deletions test/mocking.getter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ describe("mocking", () => {
mockedFoo = mock(FooWithGetterAndSetter);

// then
expect((mockedFoo.twoPlusTwo as any) instanceof MethodToStub).toBe(true);
const twoPlusTwo: any = mockedFoo.twoPlusTwo;
expect(twoPlusTwo instanceof MethodToStub).toBe(true);

// cleanup, make sure to use twoPlusTwo
when(twoPlusTwo).thenReturn(4);
});

it("does create own property descriptors on instance", () => {
Expand All @@ -48,7 +52,11 @@ describe("mocking", () => {
// when

// then
expect((mockedFoo.sampleString as any) instanceof MethodToStub).toBe(true);
const sampleString: any = mockedFoo.sampleString;
expect(sampleString instanceof MethodToStub).toBe(true);

// cleanup, make sure to use twoPlusTwo
when(sampleString).thenReturn("x");
});

it("does create inherited property descriptors on instance", () => {
Expand Down
12 changes: 8 additions & 4 deletions test/mocking.properties.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ describe("mocking", () => {
when(mockedFoo.sampleNumber).thenReturn(42);

// then
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();
const sampleNumber: any = mockedFoo.sampleNumber;
expect(sampleNumber.methodStubCollection).toBeDefined();
expect(sampleNumber.matchers).toBeDefined();
expect(sampleNumber.mocker).toBeDefined();
expect(sampleNumber.methodName).toBeDefined();

// cleanup, make sure to use sampleNumber
when(sampleNumber).thenReturn(0);
});

it("does create own property descriptors on instance", () => {
Expand Down
24 changes: 20 additions & 4 deletions test/mocking.types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ describe("mocking", () => {
mockedFoo = mock(SampleAbstractClass);

// then
expect((mockedFoo.twoPlusTwo as any) instanceof MethodToStub).toBe(true);
const twoPlusTwo: any = mockedFoo.twoPlusTwo;
expect(twoPlusTwo instanceof MethodToStub).toBe(true);

// cleanup, make sure to use twoPlusTwo
when(twoPlusTwo).thenReturn(4);
});

it("does create own property descriptors on instance", () => {
Expand All @@ -48,7 +52,11 @@ describe("mocking", () => {
// when

// then
expect((mockedFoo.sampleString as any) instanceof MethodToStub).toBe(true);
const sampleString: any = mockedFoo.sampleString;
expect(sampleString instanceof MethodToStub).toBe(true);

// cleanup, make sure to use sampleString
when(sampleString).thenReturn("x");
});

it("does create inherited property descriptors on instance", () => {
Expand Down Expand Up @@ -100,7 +108,11 @@ describe("mocking", () => {
mockedFoo = mock(SampleGeneric);

// then
expect((mockedFoo.twoPlusTwo as any) instanceof MethodToStub).toBe(true);
const twoPlusTwo: any = mockedFoo.twoPlusTwo;
expect(twoPlusTwo instanceof MethodToStub).toBe(true);

// cleanup, make sure to use twoPlusTwo
when(twoPlusTwo).thenReturn(4);
});

it("allows to mock method with generic return type value (with IDE completion)", () => {
Expand Down Expand Up @@ -137,7 +149,11 @@ describe("mocking", () => {
// when

// then
expect((mockedFoo.sampleString as any) instanceof MethodToStub).toBe(true);
const sampleString: any = mockedFoo.sampleString;
expect(sampleString instanceof MethodToStub).toBe(true);

// cleanup, make sure to use twoPlusTwo
when(sampleString).thenReturn("x");
});

it("does create inherited property descriptors on instance", () => {
Expand Down

0 comments on commit e2b52a7

Please sign in to comment.