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?
The
getSomethingmethod has a race condition. It isn't possible to reliably test it, because it has non-deterministic behavior.The problem is that
consumeris invoked asynchronously, after the request completes. Nothing ingetSomethingensures that will happen beforereturn 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 beforeconsumeris invoked (since the request is relatively slow). Once the request completes, it will set the field insomeEntity1, but by this point,getSomethinghas already returned the incorrect value to the caller, and the object referenced bysomeEntity1won't be read again.The correct way to handle this is to make
getSomethingalso return aCompletionStage:Then, to unit test this, you can create mocks for
WebTarget,Invocation.Builder,CompletionStageRxInvoker, andResponseas you have. Rather than mockingCompletionStage, it will be simpler to have the mockedcompletionStageRxInvoker.get()method returnCompletableFuture.completedFuture(mockResponse). Note thatCompletableFutureis a concrete implementation ofCompletionStagethat 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:
Where
apiClientis an injected instance of a custom class or interface that you can mock, with a method declared like this: