attq/test/retries.test.ts

103 lines
3.1 KiB
TypeScript
Raw Permalink Normal View History

2026-02-06 07:59:19 +00:00
import { withRetry } from "../src/retries";
2026-02-03 01:44:43 +00:00
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 () => {
2026-02-06 07:59:19 +00:00
// Don't enable the `advanceTimers` configuration or else risk skewing the
// timing scheme used in this test.
2026-02-03 01:44:43 +00:00
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!")
2026-02-06 07:59:19 +00:00
// Mock timers without auto-advance enabled are a pain to manage when merely
// counting retries, so use real timers and a very short backoff duration in
// other tests instead.
2026-02-03 01:44:43 +00:00
jest.useRealTimers();
});