attq/src/retries.test.ts
2026-02-03 05:05:11 +00:00

99 lines
2.9 KiB
TypeScript

import { withRetry } from "./retries";
const FAST_RETRY_CONFIG: Parameters<typeof withRetry>[1] = {
attempts: 6,
// Always retry almost immediately, even when using real timers.
backoffMs: () => 1,
};
test("successful function only executes once", async () => {
const fn = jest.fn(async () => "success!");
const result = await withRetry(fn, FAST_RETRY_CONFIG)();
expect(result).toEqual("success!");
expect(fn).toHaveBeenCalledTimes(1);
});
test("failing function rejects after correct number of attempts", async () => {
const fn = jest.fn(async () => {
throw "oops!";
});
await expect(withRetry(fn, FAST_RETRY_CONFIG)()).rejects.toEqual("oops!");
expect(fn).toHaveBeenCalledTimes(FAST_RETRY_CONFIG.attempts);
});
test("parameters are passed through", async () => {
const fn = async (str: string, n: number) => {
let result = "";
for (let i = 0; i < n; i += 1) {
result += str;
}
return result;
};
const retriable = withRetry(fn, FAST_RETRY_CONFIG);
// A bit of type system hackery to assert at build time that function
// signatures match.
/* eslint-disable @typescript-eslint/no-unused-vars */
type Assert<_T extends true> = void;
type _ = Assert<typeof retriable extends typeof fn ? true : false>;
/* eslint-enable @typescript-eslint/no-unused-vars */
await expect(retriable("hello;", 3)).resolves.toEqual("hello;hello;hello;");
});
test("retries back off with expected delays", async () => {
jest.useFakeTimers();
let n = 0;
const fn = jest.fn(async () => {
if (n < 5) {
n += 1;
throw "oops!";
} else {
// Fake timers don't play nicely with the `expect().rejects` API, so
// ensure that the last attempt succeeds.
return "success!";
}
});
const promise = withRetry(
fn,
{
attempts: 6,
backoffMs: (attempt) => 1000 * 2 ** attempt,
},
)();
expect(fn).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(100);
expect(fn).toHaveBeenCalledTimes(1);
await jest.advanceTimersByTimeAsync(1000);
expect(fn).toHaveBeenCalledTimes(2);
await jest.advanceTimersByTimeAsync(100);
expect(fn).toHaveBeenCalledTimes(2);
await jest.advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenCalledTimes(3);
await jest.advanceTimersByTimeAsync(100);
expect(fn).toHaveBeenCalledTimes(3);
await jest.advanceTimersByTimeAsync(4000);
expect(fn).toHaveBeenCalledTimes(4);
await jest.advanceTimersByTimeAsync(100);
expect(fn).toHaveBeenCalledTimes(4);
await jest.advanceTimersByTimeAsync(8000);
expect(fn).toHaveBeenCalledTimes(5);
await jest.advanceTimersByTimeAsync(100);
expect(fn).toHaveBeenCalledTimes(5);
await jest.advanceTimersByTimeAsync(16000);
expect(fn).toHaveBeenCalledTimes(6);
await expect(promise).resolves.toEqual("success!")
// Fake timers don't play nicely with the `expect().rejects` API, so disable
// them again for other tests.
jest.useRealTimers();
});