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 :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
- We will create the implementation using JUnit rules option :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
- sample of how you can use it :
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:" + 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 .
Absolutely brilliant! Loved it!
LikeLike