How to test interactions inside of a Futures `whenComplete` method

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?

  • awaitility.org might be helpful, if you cannot wait for your future. It polls state with a specified timeout.

    – 

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.

Leave a Comment