Issue
I am writing unit test for a method which uses WebClient to call a REST end point. Using MockWebServer, I am able to cover the code for a success response, but I am unable to find any way to simulate error response so that the related code pertaining to error handling also gets covered in unit tests.
source class:
@Service
@Slf4j
public class EmployeeService {
private final WebClient webClient;
private final PaymentServiceConfiguration paymentServiceConfiguration;
public EmployeeService(WebClient webClient, PaymentServiceConfiguration paymentServiceConfiguration){
this.webClient = webClient;
this.paymentServiceConfiguration= paymentServiceConfiguration;
}
public Mono<PaymentDataResponse> getPaymentInfo(PaymentDataRequest paymentDataRequest){
return webClient
.post()
.uri(paymentServiceConfiguration.getBaseUri() + "/payment")
.body(Mono.just(paymentDataRequest, PaymentDataRequest.class)
.retrieve()
.onStatus(HttpStatus::isError, this.handleErrorResponse)
.bodyToMono(PaymentDataResponse.class)
.doOnError((throwable)-> {
log.error("Exception in getPaymentInfo method. Message {}", throwable.getMessage());
throw(Exceptions.propagate(throwable));
});
}
private Mono<? extends Throwable> handleErrorResponse(ClientResponse clientResponse){
Mono<String> errorMessage = clientResponse.bodyToMono(String.class);
return errorMessage.flatMap((message) -> {
log.error("Error response code is: {}, and the message is: {} ", clientResponse.rawStatusCode(), message);
return Mono.error(new RuntimeException(message));
});
}
}
Test Class:
@RunWith(MockitoJunitRunner.class)
public class EmployeeServiceTest {
private MockWebServer server;
private EmployeeService employeeService;
private PaymentServiceConfiguration paymentServiceConfiguration;
private String paymentServiceUri;
@Before
public void setUp() throws IOException{
paymentServiceConfiguration = mock(PaymentServiceConfiguration.class);
paymentServiceUri = "/paymentdomain.com";
employeeService = new EmployeeService(WebClient.create(), paymentServiceConfiguration);
server = new MockWebServer();
server.start();
}
@Test
public testPaymentInfo() throws IOException, InterruptedException {
when(paymentServiceConfiguration.getBaseUri()).thenReturn(server.url(paymentServiceUri).url.toString());
String result = "{\"paymentId\": 101, "\paymentType\": \"online\"}";
server.enque(
new MockResponse.setResponseCode(200)
.setHeader("content-type", "application/json")
.setBody(result);
);
StepVerifier.create(employeeService.getPaymentInfo(new PaymentDataRequest())
.expectNext(new ObjectMapper.readValue(result, PaymentDataResponse.class))
.expectComplete()
.verify();
RecordRequest request = server.takeRequest();
Assert.assertEquals("POST", request.getMethod());
Assert.assertEquals("/paymentdomain.com/payment", request.getPath());
}
@After
public void tearDown() throws IOException {
server.shutdown();
}
}
Above test covers the happy path of code and marks the code coverage appropriately. How can I mock an error here, so that it can cover the below lines in source class code (regarding error scenario)
// .onStatus(HttpStatus::isError, this.handleErrorResponse)
// .doOnError((throwable)-> { ......
Solution
You need to provide error status 4xx or 5xxx. For example,
server.enqueue(
new MockResponse().setResponseCode(500)
.setHeader("content-type", "application/json")
.setBody("{}");
);
StepVerifier.create(employeeService.getPaymentInfo(new PaymentDataRequest())
.expectError()
.verify();
ps
I would recommend looking at WireMock which provides a very good API for testing web clients matchig . You could test posittive and negative scanarios including timeouts, retry logic using Scenarios or even simulate timeouts using Delays.
Answered By - Alex
Answer Checked By - Terry (JavaFixing Volunteer)