Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save felipepodesta/11a2e9aa83e992258e5e0eed89e3d20c to your computer and use it in GitHub Desktop.
Save felipepodesta/11a2e9aa83e992258e5e0eed89e3d20c to your computer and use it in GitHub Desktop.
Nest: testing (E2E) asynchronous side-effects with Jest and RxJs
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);
}
});
});
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