I have a class thats a wrapper around the Lettuce Redis client that puts objects into redis and records a few metrics when the future completes inside of its whenComplete
. The class under test looks like so,
@Slf4j
@AllArgsConstructor
public class redisWriter {
/**
* The connection used to connect to Elasticache.
*/
private final StatefulRedisConnection<byte[], byte[]> elasticacheConnection;
/**
* Metric Util for publishing custom metrics.
*/
private MetricsUtil metricsUtil;
public void putRecord(final byte[] key, final byte[] value, final int ttlSeconds) {
final Duration ttlDuration = Duration.ofSeconds(ttlSeconds);
final SetArgs ttlSetArgs = SetArgs.Builder.ex(ttlDuration);
final long startTime = System.currentTimeMillis();
this.elasticacheConnection
.async()
.set(key, value, ttlSetArgs)
.whenComplete((msg, exception) -> {
this.metricsUtil.elasticacheInteractionTime("PutRecord", startTime);
if (exception != null) {
if (exception instanceof RedisCommandTimeoutException) {
this.metricsUtil.elasticacheTimeout();
} else if (exception instanceof RedisException) {
log.error("Something went wrong putting in a record", exception);
this.metricsUtil.elasticacheError("PutRecord");
}
}
});
}
}
My problem is how do I test the interactions with metricsUtil
? I want to make sure that the error handling is correct in the unit tests but I have not figured out a good method for doing so. The restriction that is killing me is I can’t just return the future and force it complete in the unit test, I need the future to complete itself without more interaction then just calling putRecord
. Is this possible to do with Mockito or will I need to do some refactoring?
I was able to get it working by having the mocks return complete futures like so
final AsyncCommand<byte[], byte[], String> asyncReturnStringCommand =
new AsyncCommand<>(this.mockRedisReturnStringCommand);
asyncReturnStringCommand.complete("OK");
Mockito.when(this.mockRedisAsyncCommands.set(eq(KEY), eq(VALUE), this.setArgsArgumentCaptor.capture()))
.thenReturn(asyncReturnStringCommand);
in the setups, and as a example of how to get it to throw a exception,
@Test
public void testPutRecordTimeout() {
this.haveRedisSetFailWithException(new RedisCommandTimeoutException("timeout"));
this.elasticacheWriterDao.putRecord(KEY, VALUE, TTL_SECONDS);
Mockito.verify(this.mockKdrsMetricUtil, times(1)).elasticacheTimeout();
Mockito.verify(this.mockKdrsMetricUtil, times(1))
.elasticacheInteractionTime(eq(ELASTICACHE_PUT_RECORD), anyLong());
}
private void haveRedisSetFailWithException(@Nonnull final Throwable exception) {
final AsyncCommand<byte[], byte[], String> asyncReturnStringCommand =
new AsyncCommand<>(this.mockRedisReturnStringCommand);
asyncReturnStringCommand.completeExceptionally(exception);
Mockito.when(this.mockRedisAsyncCommands.set(eq(KEY), eq(VALUE), this.setArgsArgumentCaptor.capture()))
.thenReturn(asyncReturnStringCommand);
}
So its completed before it the test even starts in earnest.
awaitility.org might be helpful, if you cannot wait for your future. It polls state with a specified timeout.