Recently I tried to test the retry behaviour in unit tests by setting up the mocked dependency to return multiple Mono.error() before returning a successful mono.just() at the end:
@Mock
Dependency dependency;
@InjectMocks
ClassUnderTest classUnderTest;
@Test
void someTest() {
final Object object = new Object();
when(dependency.method(anyString()))
.thenReturn(Mono.error(new Exception()))
.thenReturn(Mono.error(new Exception()))
.thenReturn(Mono.error(new Exception()))
.thenReturn(Mono.just(object));
StepVerifier.create(classUnderTest.method("abc"))
.expectNext(object)
.verifyComplete();
verify(dependency, times(4)).method("abc");
}
The above setup won't work as I figured out later, the retry in Reactor is not done via recalling the method for specific amount of time, but by calling the method once, gets the publisher, and re-subscribing to it again and again.
class ClassUnderTest {
private Dependency dependency;
public Mono<Object> method(final String str) {
return this.dependency.method(str).retryWhen(Retry.max(3));
}
}
And re-subscribing won't work, if the Dependency#method
is implemented as:
class Dependency {
private OtherDependency otherDependency;
public Mono<Object> method(final String str) {
return this.otherDependency.get(str).map(/* some mapping logic */);
}
}
Dependency#method
cannot make too much assumption on whether or not OtherDependency#get
is deferred. Hence, Dependency
needs to:
class Dependency {
private OtherDependency otherDependency;
public Mono<Object> method(final String str) {
return Mono.defer(() -> this.otherDependency.get(str)).map(/* some mapping logic */);
}
}
Since we want to say that each method should be "retry-able", does that mean, we need to always use defer(...)
?
Or am I mis-understanding anything?
I should have thought about it.
The easier way instead of making all methods natively "retry-able", is to wrap the publisher with defer
before attaching the retryWhen
operator.
Before:
class ClassUnderTest {
private Dependency dependency;
public Mono<Object> method(final String str) {
return dependency.method(str).retryWhen(Retry.max(3));
}
}
After:
class ClassUnderTest {
private Dependency dependency;
public Mono<Object> method(final String str) {
return Mono.defer(() -> dependency.method(str)).retryWhen(Retry.max(3));
}
}
Now, we do not have to say all methods should be natively "retry-able", but to always wrap the to-be-retried publisher with defer
.