Skip to content

Commit 93f728c

Browse files
committed
test: add unit tests for retryWithBackoff
1 parent 1c89f28 commit 93f728c

1 file changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* MusicBlocks
3+
*
4+
* @copyright 2026 Music Blocks contributors
5+
* @author Sapnil Biswas
6+
*
7+
* @license
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*/
21+
22+
const { retryWithBackoff } = require("../retryWithBackoff");
23+
24+
describe("retryWithBackoff", () => {
25+
let mockCheck;
26+
let mockOnSuccess;
27+
let mockOnRetry;
28+
let mockDelayFn;
29+
30+
beforeEach(() => {
31+
mockCheck = jest.fn();
32+
mockOnSuccess = jest.fn().mockResolvedValue();
33+
mockOnRetry = jest.fn();
34+
mockDelayFn = jest.fn().mockResolvedValue();
35+
});
36+
37+
it("should succeed immediately if check returns truthy", async () => {
38+
mockCheck.mockReturnValue("success_data");
39+
40+
const result = await retryWithBackoff({
41+
check: mockCheck,
42+
onSuccess: mockOnSuccess,
43+
delayFn: mockDelayFn
44+
});
45+
46+
expect(result).toBe("success_data");
47+
expect(mockCheck).toHaveBeenCalledTimes(1);
48+
expect(mockOnSuccess).toHaveBeenCalledWith("success_data");
49+
expect(mockDelayFn).not.toHaveBeenCalled();
50+
});
51+
52+
it("should retry and succeed when check eventually returns truthy", async () => {
53+
mockCheck
54+
.mockReturnValueOnce(false)
55+
.mockReturnValueOnce(null)
56+
.mockReturnValueOnce({ data: "yes" });
57+
58+
const result = await retryWithBackoff({
59+
check: mockCheck,
60+
onSuccess: mockOnSuccess,
61+
onRetry: mockOnRetry,
62+
delayFn: mockDelayFn,
63+
initialDelay: 10
64+
});
65+
66+
expect(result).toEqual({ data: "yes" });
67+
expect(mockCheck).toHaveBeenCalledTimes(3);
68+
expect(mockOnSuccess).toHaveBeenCalledWith({ data: "yes" });
69+
expect(mockOnRetry).toHaveBeenCalledTimes(2);
70+
71+
// Attempt 0
72+
expect(mockOnRetry).toHaveBeenNthCalledWith(1, 0);
73+
// Attempt 1
74+
expect(mockOnRetry).toHaveBeenNthCalledWith(2, 1);
75+
76+
expect(mockDelayFn).toHaveBeenCalledTimes(2);
77+
// 10 * 2^0 = 10
78+
expect(mockDelayFn).toHaveBeenNthCalledWith(1, 10);
79+
// 10 * 2^1 = 20
80+
expect(mockDelayFn).toHaveBeenNthCalledWith(2, 20);
81+
});
82+
83+
it("should throw an error after exceeding maxRetries", async () => {
84+
mockCheck.mockReturnValue(false);
85+
86+
await expect(
87+
retryWithBackoff({
88+
check: mockCheck,
89+
onSuccess: mockOnSuccess,
90+
delayFn: mockDelayFn,
91+
maxRetries: 3,
92+
errorMessage: "Failed to get bounds"
93+
})
94+
).rejects.toThrow("Failed to get bounds");
95+
96+
// check should be called maxRetries + 1 times (count from 0 to 3)
97+
expect(mockCheck).toHaveBeenCalledTimes(4);
98+
expect(mockOnSuccess).not.toHaveBeenCalled();
99+
expect(mockDelayFn).toHaveBeenCalledTimes(4);
100+
});
101+
102+
it("should use default error message when exceeding maxRetries without custom message", async () => {
103+
mockCheck.mockReturnValue(false);
104+
105+
await expect(
106+
retryWithBackoff({
107+
check: mockCheck,
108+
onSuccess: mockOnSuccess,
109+
delayFn: mockDelayFn,
110+
maxRetries: 1
111+
})
112+
).rejects.toThrow("Retry limit exceeded");
113+
});
114+
115+
it("should use default delay function if none provided", async () => {
116+
jest.useFakeTimers();
117+
118+
mockCheck.mockReturnValueOnce(false).mockReturnValueOnce(true);
119+
120+
const promise = retryWithBackoff({
121+
check: mockCheck,
122+
onSuccess: mockOnSuccess,
123+
initialDelay: 50
124+
});
125+
126+
// Advance timers by 50ms to resolve the default delay (50 * 2^0 = 50ms)
127+
jest.advanceTimersByTime(50);
128+
129+
await promise;
130+
expect(mockCheck).toHaveBeenCalledTimes(2);
131+
132+
jest.useRealTimers();
133+
});
134+
135+
it("should use default maxRetries and initialDelay if not provided", async () => {
136+
mockCheck.mockReturnValue(false);
137+
138+
await expect(
139+
retryWithBackoff({
140+
check: mockCheck,
141+
onSuccess: mockOnSuccess,
142+
delayFn: mockDelayFn
143+
})
144+
).rejects.toThrow("Retry limit exceeded");
145+
146+
// default maxRetries is 20, so 21 attempts
147+
expect(mockCheck).toHaveBeenCalledTimes(21);
148+
149+
// Check default initialDelay (50 * 2^0 = 50)
150+
expect(mockDelayFn).toHaveBeenNthCalledWith(1, 50);
151+
});
152+
});

0 commit comments

Comments
 (0)