Implement Retry in Unit test

If you are doing an integration test or sanity testing for real application endpoints in continuous delivery process where you hot deploy the application and and you want to trigger some checkups and sanity testing part of your delivery pipeline , how you can add retry logic with delay to unit testing and to your integration test ?

Sample app that include a working demo for it at the following :

https://github.com/Romeh/spring-boot-sample-app

So I will through about how you can do it :

  • We will create annotation to mark the test cases needed to be retried :


package com.test.SpringBootSample.retry;
/**
* Created by id961900 on 05/09/2017.
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Retries a unit-test according to the attributes set here
* <p>
* The class containing the test(s) decorated with this annotation must have a public field of type {@link RetryRule}
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Retry {
/**
* @return the number of times to try this method before the failure is propagated through
*/
int times() default 3;
/**
* @return how long to sleep between invocations of the unit tests, in milliseconds
*/
long timeout() default 0;
}

view raw

Retry.java

hosted with ❤ by GitHub

  • We will create the implementation using JUnit rules option :


public final class RetryRule implements TestRule {
@NotNull
private Throwable[] errors = new Throwable[0];
private int currentAttempt = 0;
@Override
public Statement apply(final Statement base, final Description description) {
final Retry retryAnnotation = description.getAnnotation(Retry.class);
if (retryAnnotation == null) {
return base;
}
final int times = retryAnnotation.times();
if (times <= 0) {
throw new IllegalArgumentException(
"@" + Retry.class.getSimpleName() + " cannot be used with a \"times\" parameter less than 1"
);
}
final long timeout = retryAnnotation.timeout();
if (timeout < 0) {
throw new IllegalArgumentException(
"@" + Retry.class.getSimpleName() + " cannot be used with a \"timeout\" parameter less than 0"
);
}
errors = new Throwable[times];
return new Statement() {
@Override
public void evaluate() throws Throwable {
while (currentAttempt < times) {
try {
base.evaluate();
return;
} catch (Throwable t) {
errors[currentAttempt] = t;
currentAttempt++;
Thread.sleep(timeout);
}
}
throw RetryException.from(errors);
}
};
}
/**
* @return an array representing the errors that have been encountered so far. {@code errors()[0]} corresponds to the
* Throwable encountered when running the test-case for the first time, {@code errors()[1]} corresponds to the
* Throwable encountered when running the test-case for the second time, and so on.
*/
@NotNull
public Throwable[] errors() {
return Arrays.copyOfRange(errors, 0, currentAttempt);
}
/**
* A convenience method to return the {@link Throwable} that was encountered on the last invocation of this test-case.
* Returns {@code null} if this is the first invocation of the test-case.
*/
public Throwable lastError() {
final int currentAttempt = currentAttempt();
final Throwable[] errors = errors();
if (currentAttempt == 0) {
return null;
}
return errors[currentAttempt – 1];
}
/**
* @return the current attempt (0-indexed). 0 is the very first attempt, 1 is the next one, and so on.
*/
public int currentAttempt() {
return currentAttempt;
}
}

view raw

RetryRule.java

hosted with ❤ by GitHub

  • sample of how you can use it :


public class ApplicationSanityCheck{
@Rule
public final RetryRule retry = new RetryRule();
private int port = 8080;
private RestTemplate template;
private URL base;
@Before
public void setUp() throws Exception {
this.base = new URL("http://localhost:&quot; + port + "/");
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
template = new RestTemplate(requestFactory);
}
@Test
// example of true end to end call which call UAT real endpoint
// and retry in case of failure 4 times with 20 seconds delay between each try
@Retry(times = 4, timeout = 20000)
public void test_is_server_up() {
assertTrue(template.getForEntity(base + "/health", String.class).getStatusCode().is2xxSuccessful());
}
}

Then when you execute the test and your endpoint is not reachable right away , it will retry again based into your retry configuration .

One comment

Leave a Reply