Last active
June 1, 2022 13:41
-
-
Save VinceOPS/56e25ed3f22822202ec92abeb80eaa80 to your computer and use it in GitHub Desktop.
Nest: testing (E2E) asynchronous side-effects with Jest and RxJs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { TimeoutError } from 'rxjs'; | |
import { waitForAssertion } from './wait-for-assertion'; | |
describe('waitForAssertion', () => { | |
let mockWitness: jest.Mock; | |
beforeEach(() => { | |
mockWitness = jest.fn().mockReturnValue(false); | |
}); | |
describe('Waits both synchronous and asynchronous callbacks', () => { | |
let intervalsCount: number; | |
let intervalDelay: number; | |
let delayBeforeSuccessTrigger: number; | |
let timeoutDelay: number; | |
beforeEach(() => { | |
intervalsCount = 4; | |
intervalDelay = 20; | |
delayBeforeSuccessTrigger = (intervalsCount + 1) * intervalDelay; | |
// safety margin: use more than the expected execution time as timeout delay | |
timeoutDelay = 3 * intervalsCount * intervalDelay; | |
}); | |
it('waits for an asynchronous assertion to succeed', async () => { | |
setTimeout(() => mockWitness.mockReturnValue(true), delayBeforeSuccessTrigger); | |
await waitForAssertion(() => expect(mockWitness()).toBe(true), timeoutDelay, intervalDelay); | |
// "success" is triggered after `intervalsCount` + 1 cycles, so expect at least `intervalCount` calls | |
expect(mockWitness.mock.calls.length).toBeGreaterThanOrEqual(intervalsCount); | |
}); | |
it('also works with async closures', async () => { | |
setTimeout(() => mockWitness.mockResolvedValue(true), delayBeforeSuccessTrigger); | |
await waitForAssertion(() => expect(mockWitness()).resolves.toEqual(true), timeoutDelay, intervalDelay); | |
expect(mockWitness.mock.calls.length).toBeGreaterThanOrEqual(intervalsCount); | |
}); | |
}); | |
it('throws a TimeoutError if the assertion did not succeed', async () => { | |
const expectedCyclesCount = 4; | |
const intervalDelay = 100; | |
const timeoutDelay = (expectedCyclesCount + 1) * intervalDelay; | |
expect.assertions(expectedCyclesCount + 1); | |
try { | |
await waitForAssertion(() => expect(mockWitness()).toBe(true), timeoutDelay, intervalDelay); | |
} catch (e) { | |
expect(e).toBeInstanceOf(TimeoutError); | |
} | |
}); | |
it('throws forward instances of TypeError', async () => { | |
try { | |
await waitForAssertion(() => { | |
const failedInjectionService: any = {}; | |
failedInjectionService.foo(); | |
}); | |
} catch (e) { | |
expect(e).toBeInstanceOf(TypeError); | |
} | |
}); | |
it('throws forward instances of ReferenceError', async () => { | |
try { | |
await waitForAssertion(() => { | |
// @ts-ignore | |
b.foo(); | |
}); | |
} catch (e) { | |
expect(e).toBeInstanceOf(ReferenceError); | |
} | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { from, interval, throwError } from 'rxjs'; | |
import { catchError, first, switchMap, timeout } from 'rxjs/operators'; | |
/** | |
* Wait for any assertion or group of assertions to succeed, or timeout. | |
* | |
* Run (through a promise) the given function `assertion` every `intervalDelay` milliseconds | |
* until it stops throwing or until `timeoutDelay` is passed (timeout). | |
* | |
* @param assertion Closure containing all assertions to be made (calls of `expect()`). | |
* @param timeoutDelay How long should the assertion be repeated until it passes (or times out). | |
* @param intervalDelay How often should the assertion be repeated during `timeoutDelay`. | |
* | |
* @return Resolve on success, or reject with a `TimeoutError`. | |
* | |
* @example | |
* // in a test, where we need to ensure a value is asynchronously updated in elasticsearch | |
* await waitForAssertion(async () => { | |
* const { document } = await elasticsearchService.get(UserIndex, userId); | |
* return expect(document.firstName).toBe(updatedUser.firstName); | |
* }); | |
*/ | |
export function waitForAssertion(assertion: () => any, timeoutDelay: number = 1000, intervalDelay: number = 100) { | |
return interval(intervalDelay) | |
.pipe( | |
switchMap(() => from(Promise.resolve(assertion()))), | |
catchError((err, o) => (err instanceof ReferenceError || err instanceof TypeError ? throwError(err) : o)), | |
first(), | |
timeout(timeoutDelay), | |
) | |
.toPromise(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment