RetryableCommandExecutorTest.java
package redis.clients.jedis.executors;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import redis.clients.jedis.CommandObject;
import redis.clients.jedis.Connection;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.providers.ConnectionProvider;
@ExtendWith(MockitoExtension.class)
public class RetryableCommandExecutorTest {
@Mock
private ConnectionProvider mockProvider;
@Mock
private Connection mockConnection;
@Mock
private CommandObject<String> mockCommandObject;
@Test
public void testConstructorWithNullProvider() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> new RetryableCommandExecutor(null, 3, Duration.ofSeconds(1)),
"Should throw IllegalArgumentException when provider is null"
);
assertTrue(exception.getMessage().contains("provider"),
"Exception message should mention 'provider'");
}
@Test
public void testConstructorWithInvalidMaxAttempts() {
// Test with zero
IllegalArgumentException exceptionZero = assertThrows(
IllegalArgumentException.class,
() -> new RetryableCommandExecutor(mockProvider, 0, Duration.ofSeconds(1)),
"Should throw IllegalArgumentException when maxAttempts is zero"
);
assertTrue(exceptionZero.getMessage().contains("maxAttempts"),
"Exception message should mention 'maxAttempts'");
// Test with negative value
IllegalArgumentException exceptionNegative = assertThrows(
IllegalArgumentException.class,
() -> new RetryableCommandExecutor(mockProvider, -1, Duration.ofSeconds(1)),
"Should throw IllegalArgumentException when maxAttempts is negative"
);
assertTrue(exceptionNegative.getMessage().contains("maxAttempts"),
"Exception message should mention 'maxAttempts'");
}
@Test
public void testValidConstruction() {
// Should not throw any exceptions
assertDoesNotThrow(() -> new RetryableCommandExecutor(mockProvider, 1, Duration.ofSeconds(1)));
assertDoesNotThrow(() -> new RetryableCommandExecutor(mockProvider, 3, Duration.ZERO));
assertDoesNotThrow(() -> new RetryableCommandExecutor(mockProvider, 10, Duration.ofMinutes(5)));
}
@Test
public void testMaxAttemptsIsRespected() throws Exception {
// Set up the mock to return a connection but throw an exception when executing
when(mockProvider.getConnection(any())).thenReturn(mockConnection);
when(mockConnection.executeCommand(any(CommandObject.class))).thenThrow(new JedisConnectionException("Connection failed"));
// Create the executor with exactly 3 attempts
final int maxAttempts = 3;
RetryableCommandExecutor executor = spy(new RetryableCommandExecutor(mockProvider, maxAttempts, Duration.ofSeconds(10)));
// Mock the sleep method to avoid actual sleeping
doNothing().when(executor).sleep(anyLong());
// Execute the command and expect an exception
assertThrows(JedisException.class, () -> executor.executeCommand(mockCommandObject));
// Verify that we tried exactly maxAttempts times
verify(mockProvider, times(maxAttempts)).getConnection(any());
verify(mockConnection, times(maxAttempts)).close();
}
@Test
public void testExecuteCommandWithNoRetries() throws Exception {
// Set up the mock to return a connection and have it execute the command successfully
when(mockProvider.getConnection(any())).thenReturn(mockConnection);
when(mockConnection.executeCommand(mockCommandObject)).thenReturn("success");
// Create the executor with just 1 attempt (no retries)
RetryableCommandExecutor executor = new RetryableCommandExecutor(mockProvider, 1, Duration.ofSeconds(1));
// Execute the command
String result = executor.executeCommand(mockCommandObject);
// Verify the result and that the connection was closed
assertEquals("success", result);
verify(mockConnection, times(1)).close();
verify(mockProvider, times(1)).getConnection(any());
}
@Test
public void testMaxAttemptsExceeded() throws Exception {
// Set up the mock to return a connection but throw an exception when executing
when(mockProvider.getConnection(any())).thenReturn(mockConnection);
when(mockConnection.executeCommand(any(CommandObject.class))).thenThrow(new JedisConnectionException("Connection failed"));
// Create the executor with 3 attempts
RetryableCommandExecutor executor = spy(new RetryableCommandExecutor(mockProvider, 3, Duration.ofSeconds(1)));
// Mock the sleep method to avoid actual sleeping
doNothing().when(executor).sleep(anyLong());
// Execute the command and expect an exception
JedisException exception = assertThrows(
JedisException.class,
() -> executor.executeCommand(mockCommandObject),
"Should throw JedisException when max attempts are exceeded"
);
// Verify the exception and that we tried the correct number of times
assertEquals("No more attempts left.", exception.getMessage());
assertEquals(1, exception.getSuppressed().length);
verify(mockProvider, times(3)).getConnection(any());
verify(mockConnection, times(3)).close();
}
}