diff --git a/README.md b/README.md index 5b8f528..0d78f81 100644 --- a/README.md +++ b/README.md @@ -351,6 +351,16 @@ foo.bar(); console.log(capture(spiedFoo.bar).last()); // [42] ``` +### Verify with timeout + +This feature is useful when testing asynchronous functionality. You do some action and expect the result to arrive as an asynchronous function call to one of your mocks. + +```typescript +let mockedFoo:Foo = mock(Foo); +await verify(mockedFoo.getBar(3)).timeout(1000); +``` + + ### Thanks * Szczepan Faber (https://www.linkedin.com/in/szczepiq) diff --git a/src/MethodStubVerificator.ts b/src/MethodStubVerificator.ts index 6bfceb2..dd4be33 100644 --- a/src/MethodStubVerificator.ts +++ b/src/MethodStubVerificator.ts @@ -1,6 +1,9 @@ import {MethodToStub} from "./MethodToStub"; import {MethodCallToStringConverter} from "./utils/MethodCallToStringConverter"; +// Save reference to setTimeout, in case tests are mocking time functions +const localSetTimeout = setTimeout; + export class MethodStubVerificator { private methodCallToStringConverter: MethodCallToStringConverter = new MethodCallToStringConverter(); @@ -91,4 +94,25 @@ export class MethodStubVerificator { throw new Error(`${errorBeginning}but none of them has been called.`); } } + + public timeout(ms: number): Promise { + return new Promise((resolve, reject) => { + const expired = Date.now() + ms; + + const check = () => { + const allMatchingActions = this.methodToVerify.mocker.getAllMatchingActions(this.methodToVerify.name, this.methodToVerify.matchers); + + if (allMatchingActions.length > 0) { + resolve(); + } else if (Date.now() >= expired) { + const methodToVerifyAsString = this.methodCallToStringConverter.convert(this.methodToVerify); + reject(new Error(`Expected "${methodToVerifyAsString}to be called within ${ms} ms.`)); + } else { + localSetTimeout(check, 1); + } + }; + + check(); + }); + } } diff --git a/test/verification.spec.ts b/test/verification.spec.ts index 9287f7b..5bb0a98 100644 --- a/test/verification.spec.ts +++ b/test/verification.spec.ts @@ -1,4 +1,4 @@ -import {instance, mock, verify} from "../src/ts-mockito"; +import {instance, mock, verify, when} from "../src/ts-mockito"; import {MethodCallToStringConverter} from "../src/utils/MethodCallToStringConverter"; import {Bar} from "./utils/Bar"; import {Foo} from "./utils/Foo"; @@ -774,6 +774,59 @@ describe("verifying mocked object", () => { }); }); }); + + describe("with timeout", () => { + it("should succeed if call already happend", async () => { + // given + foo.getBar(); + + // when + await verify(mockedFoo.getBar()).timeout(10000); + + // then + verify(mockedFoo.getBar()).once(); + }); + + it("should wait for call to happen", async () => { + // given + setTimeout(() => foo.getBar(), 10); + + // when + await verify(mockedFoo.getBar()).timeout(10000); + + // then + verify(mockedFoo.getBar()).once(); + }); + + it("should fail if call does not happen", async () => { + // given + + // when + let error; + try { + await verify(mockedFoo.getBar()).timeout(10); + } catch (e) { + error = e; + } + + // then + expect(error.message).toContain("to be called within"); + }); + + it("should not alter call expectations", async () => { + // given + let result: string; + when(mockedFoo.getBar()).thenReturn("abc"); + setTimeout(() => result = foo.getBar(), 10); + + // when + await verify(mockedFoo.getBar()).timeout(10000); + + // then + expect(result).toEqual("abc"); + verify(mockedFoo.getBar()).once(); + }); + }); }); function verifyCallCountErrorMessage(error, expectedCallCount, receivedCallCount): void {