From 582284a36bd01207470e694bf982645c0761edd9 Mon Sep 17 00:00:00 2001 From: Brent Schroeter Date: Tue, 3 Feb 2026 01:44:43 +0000 Subject: [PATCH] add unit tests for withRetry() --- src/retries.test.ts | 99 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/retries.test.ts diff --git a/src/retries.test.ts b/src/retries.test.ts new file mode 100644 index 0000000..3e4fcb0 --- /dev/null +++ b/src/retries.test.ts @@ -0,0 +1,99 @@ +import { withRetry } from "./retries"; + +const FAST_RETRY_CONFIG: Parameters[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; + /* 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(); +});