Issue
I want to create a unit test for the following class:
@Service
public class XService{
public String getSomething(String inputField) {
final SomeEntity someEntity1 = new SomeEntity();
final AtomicReference<Throwable> throwable = new AtomicReference<>();
BiConsumer<Response, Throwable> consumer = (response, error) -> {
if (error != null) {
throwable.set(error);
} else {
SomeEntity someEntity2 = response.readEntity(SomeEntity.class);
someEntity1.setSomeField(someEntity2.getSomeField());
//does some stuff with the response
}
};
WebTarget target = client.target("api_url"+inputField);
target.queryParam("param", param)
.request(MediaType.APPLICATION_JSON)
.acceptLanguage(Locale.ENGLISH)
.header("Authorization", token)
.rx()
.get()
.whenCompleteAsync(consumer);
return someEntity1.getSomeField();
}
}
I have mocked everything until .whenCompleteAsync(consumer)
using something like this:
when(mockWebTarget.queryParam(any(),any())).thenReturn(mockWebTarget);
CompletionStageRxInvoker completionStageRxInvoker = mock(CompletionStageRxInvoker.class);
when(mockBuilder.rx()).thenReturn(completionStageRxInvoker);
CompletionStage<Response> mockResp = mock(CompletionStage.class);
when(completionStageRxInvoker.get()).thenReturn(mockResp);
I cannot currently change the design of the class, only make tests for it.
How can I mock the consumer object to make the code run inside the lambda? Is this even possible?
Solution
The getSomething
method has a race condition. It isn't possible to reliably test it, because it has non-deterministic behavior.
The problem is that consumer
is invoked asynchronously, after the request completes. Nothing in getSomething
ensures that will happen before return someEntity1.getSomeField()
occurs. This means that it might return the field that is copied from the read entity, or it might return the default value of that field. Most likely, it will return before consumer
is invoked (since the request is relatively slow). Once the request completes, it will set the field in someEntity1
, but by this point, getSomething
has already returned the incorrect value to the caller, and the object referenced by someEntity1
won't be read again.
The correct way to handle this is to make getSomething
also return a CompletionStage
:
public CompletionStage<String> getSomething(String inputField) {
WebTarget target = client.target("api_url"+inputField);
return target.queryParam("param", param)
.request(MediaType.APPLICATION_JSON)
.acceptLanguage(Locale.ENGLISH)
.header("Authorization", token)
.rx()
.get()
.thenApply(response -> response.readEntity(SomeEntity.class).getSomeField());
}
Then, to unit test this, you can create mocks for WebTarget
, Invocation.Builder
, CompletionStageRxInvoker
, and Response
as you have. Rather than mocking CompletionStage
, it will be simpler to have the mocked completionStageRxInvoker.get()
method return CompletableFuture.completedFuture(mockResponse)
. Note that CompletableFuture
is a concrete implementation of CompletionStage
that is part of JavaSE.
Even better, to reduce the proliferation of mocks, you can refactor this to separate out the request logic from the response-handling logic. Something like this:
public CompletionStage<String> getSomething(String inputField) {
return apiClient
.get(inputField, param, token)
.thenApply(SomeEntity::getSomeField);
}
Where apiClient
is an injected instance of a custom class or interface that you can mock, with a method declared like this:
public CompletionStage<SomeEntity> get(String inputField, Object param, String token) {
WebTarget target = client.target("api_url"+inputField);
return target.queryParam("param", param)
.request(MediaType.APPLICATION_JSON)
.acceptLanguage(Locale.ENGLISH)
.header("Authorization", token)
.rx()
.get()
.thenApply(response -> response.readEntity(SomeEntity.class));
}
Answered By - Tim Moore