I could find several questions regarding mocking a WebClient object. But I still have problems when doing a post with a body and having multiple header values. I'm just using Mockito.
public Boolean addNote(AlarmModel model) {
ServiceDTO dto = mapper(model);
return webClient.post()
.uri("/service/api/addNotes")
.headers(getHttpHeaders(dto.getHeader()))
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(dto.getBody()), ServiceBodyDTO.class)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, this::handleStatusCodeError)
.onStatus(HttpStatus::is4xxClientError, this::handleStatusCodeError)
.bodyToMono(Boolean.class)
.block();
}
And this is how I am mocking the behavior of the post method.
when(webClientMock.post()).thenReturn(requestBodyUriMock);
when(requestBodyUriMock.uri(anyString())).thenReturn(requestBodyMock);
when(requestHeadersMock.headers(any())).thenReturn(requestHeadersMock);
when(requestBodyMock.accept(any())).thenReturn(requestBodyMock);
when(requestBodyMock.contentType(any())).thenReturn(requestBodyMock);
when(requestBodyMock.bodyValue(any())).thenReturn(requestHeadersMock);
when(requestHeadersMock.retrieve()).thenReturn(responseMock);
when(responseMock.bodyToMono(Boolean.class))
.thenReturn(Mono.just(true));
But, when I execute this test case, it fails at the line having retrieve() And the exception is java.lang.NullPointerException
Did I miss anything here? TIA.
Agree with #Martin's comment that unit testing webclient has very low ROI.
I would recommend WireMock which provides very good API for testing web clients. Here are some examples
stubFor(post("/service/api/addNotes")
.withHeader(HttpHeaders.ACCEPT, containing(MediaType.APPLICATION_JSON_VALUE))
.willReturn(aResponse()
.withStatus(200)
.withBody("true")
)
);
StepVerifier.create(service.addNote(note))
.expectNextCount(1)
.verifyComplete();
You could easily test both positive and negative scenarios by providing different stubs
stubFor(post("/service/api/addNotes")
.withHeader(HttpHeaders.ACCEPT, containing(MediaType.APPLICATION_JSON_VALUE))
.willReturn(aResponse()
.withStatus(500)
)
);
test retry logic using Scenarios or even simulate timeouts using Delays
ps
not sure why you are using block but my example is using StepVerifier and assumes that addNote returns Mono<Boolean>
This is what works for me, not too different from yours. You might want to compare and feedback :)
#Mock
private WebClient webClient;
#Mock
WebClient.RequestBodyUriSpec requestBodyUriSpec;
#SuppressWarnings("rawtypes")
#Mock
WebClient.RequestHeadersSpec requestHeadersSpec;
#Mock
WebClient.RequestBodySpec requestBodySpec;
#Mock
WebClient.ResponseSpec responseSpec;
given(getWebClient()).willReturn(webClient);
given(webClient.post()).willReturn(requestBodyUriSpec);
given(requestBodyUriSpec.uri(anyString())).willReturn(requestBodySpec);
given(requestBodySpec.headers(any())).willReturn(requestBodySpec);
given(requestBodySpec.contentType(any())).willReturn(requestBodySpec);
given(requestBodySpec.accept(any())).willReturn(requestBodySpec);
given(requestBodySpec.bodyValue(any())).willReturn(requestHeadersSpec);
given(requestHeadersSpec.retrieve()).willReturn(responseSpec);
Related
My server sends a request via WebClient and the code is below:
public String getResponse(String requestBody){
...
WebClient.RequestHeadersSpec<?> request =
client.post().body(BodyInserters.fromValue(requestBody));
String resp =
request.retrieve().bodyToMono(String.class)
.doOnError(
WebClientResponseException.class,
err -> {
// do something
})
.block();
return resp;
}
I wrote a unit test for it and want to mock the WebClient so that I can receive the expected response:
when(webClientMock.post()).thenReturn(requestBodyUriMock);
when(requestBodyUriMock.body(BodyInserters.fromValue(requestBody))).thenReturn(requestHeadersMock);
when(requestHeadersMock.retrieve()).thenReturn(responseMock);
when(responseMock.bodyToMono(String.class)).thenReturn(Mono.just("response"));
String response = someServiceSpy.getResponse(requestBody);
assertEquals(Mono.just("response"), response);
However, the result is not the "response" but a html file. I think I made a mistake somewhere but I don't know how to fix it.
It seems the client referenced in your getResponse method is not set to the mock you have created (webClientMock) in your test.
If you are creating this client object in your getResponse method, I would suggest that you create it using a method that you could mock. Something like
WebClient buildWebClient() {
// build your webclient using the WebClientBuilder
}
You may want to throw a comment and or a #VisibleForTesting annotation on there so it is clear this method exists in order to make testing easier.
Then you can stub this method in your someServiceSpy:
Mockito.doReturn(mockWebClient).when(someServiceSpy).buildWebClient();
This will ensure that your mockWebClient is used in your getResponse method in your test.
Additionally, it seems as though your existing code needs a slight edit.
when(requestBodyUriMock.body(BodyInserters.fromValue(requestBody))).thenReturn(requestHeadersMock);
Should be
when(requestBodyUriMock.body(eq(BodyInserters.fromValue(requestBody)))).thenReturn(requestHeadersMock);
I have figured out the solution that mocks the WebClient directly instead of putting the build logic into a new method to mock it. I wrote my solution here in case someone else needs it future:
Let me put the code example here:
final WebClient client =
WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(someValue))
.clientConnector(new ReactorClientHttpConnector(HttpClient.create(someProvider)))
.baseUrl(someUrl)
.defaultHeader(contentType, TEXT_XML_VALUE)
.build();
final WebClient.RequestHeadersSpec<?> request =
client.post().body(BodyInserters.fromValue(reqBody));
First, we must mock the static method builder() of WebClient. If this method is not mocked, mockito can't edit the behavior of this method, and the mocked WebClient would not be used. I found this from the answer to this StackOverflow question; you can read it for more details.: How to mock Spring WebClient and builder
After mocked the builder() with the method provided by the above anwser, you will get a mocked WebClient, it's something like:
when(webClientBuilder.build()).thenReturn(webClientMock);
Then you can start to finish the rest of the work. In my sample code, the client will invoke post() and body(), so write the following:
when(requestBodyUriMock.body(any())).thenReturn(requestHeadersMock);
when(requestHeadersMock.retrieve()).thenReturn(responseMock);
when(responseMock.bodyToMono(String.class)).thenReturn(Mono.just(expectedResponse));
My unit test returned the NPE at the beginning and it because I used
when(requestBodyUriMock.body(BodyInserters.fromValue(requestBody))).thenReturn(requestHeadersMock);
instead of
when(requestBodyUriMock.body(any())).thenReturn(requestHeadersMock);
I think it is because the code not "think" the requestBodyUriMock is using the BodyInserters.fromValue(requestBody for some reasons that I haven't know yet. After I changed it to any(), it worked.
I am new to Spring Webflux and I want to know what is the best way to write a unit test to check if an error is being thrown:
public Mono<String> update(UpdateModel updateModel) {
return webClient.post()
.uri(updatePath)
.bodyValue(updateModel)
.retrieve()
.onStatus(httpStatus -> !HttpStatus.OK.equals(httpStatus),
clientResponse -> Mono.error(new ServerSideException("Failed to update")))
.bodyToMono(String.class);
}
What is the best way to write a unit test ?
This is what I am using:
#Test
public void shouldThrowServerErrorIfUpdateServiceCallFails() {
when(responseSpecMock.onStatus(any(), any())).thenReturn(responseSpecMock);
when(responseSpecMock.bodyToMono(String.class)).thenReturn(Mono.error(new ServerSideException("Failed to update")));
var response = client.update(updateModel);
StepVerifier.create(response)
.expectError(ServerSideException.class)
.verify();
}
The error is actually thrown from the lambda, not being thrown from bodyToMono call.
This is obviously not correct, but I don't know any better way to check it via unit test.
In my opinion, the best way to test how WebFlux behaves in both normal and error scenarios is to set up a test web server with Wiremock or Mountebank and avoid mocking completely. This way you are assured that your tests are exercising the WebFlux code in the same way as in real life.
Mocking and stubbing is very useful when used appropriately, but they are usually a waste of time when testing 3rd party libraries such as WebFlux. Shameless plug: I wrote an article to explain this point in more detail.
I have a Spring boot project with a service, which basically calls a private method, which does:
webClient.post().uri().accept(...).body(...).exchange()
and then puts a subscribe(...) on it, which simply logs the result.
Everything works fine, but now I need to test this, and this is where things start to get interesting.
By far, I've tried MockServer, okhttp, Spring's WebMockServer(or something), and only MockServer was willing to work at some point properly, while okhttp latest wants junit.rules.* (which is problematic to
achieve), WebMockServer specifically wants RestTemplate.
Google does give out examples, in which a webClient logic method is left without a .exchange() call, giving a chance to call .block() in the test, but I'm not willing to expose a private method just in order to workaround the async calls.
Currently I'm struggling with DEEP_STUB strategy of Mockito to mock out the actual webClient chain, but this fails to work from the box, and I'm trying to make it work while writing this question.
So the question is - is there a proper way to test a webClient with an async call (maybe a MockServer with a timeout to the verification or something)?
Wiremock seem to be an accepted way of mocking external http servers and it is now part of spring test framework.
Here is an introduction: https://www.baeldung.com/introduction-to-wiremock
Otherwise WebTestClient is a bean which might come in handy at this point if you can inject it in the constructor of your service class?
#SpringBootTest(webEnvironment = RANDOM_PORT)
#AutoConfigureWebClient
class DemoResourceTest {
#Autowired
private WebTestClient webTestClient;
#BeforeEach
public void setUp() {
webTestClient = webTestClient
.mutate()
.responseTimeout(Duration.ofMillis(1000))
.build();
}
#Test // just for reference
void some_test_that_gives_success() {
webTestClient.get()
.uri(uriBuilder -> uriBuilder
.path("/world")
.queryParam("requestString", "hello")
.build())
.accept(APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBody(String.class);
}
}
I have a simple WebFlux application (that uses controllers, not router functions). The only non-standard part is that it uses Server-Sent-Events.
An interesting part of the controller is
#GetMapping(path = "/persons", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<Object>> persons() {
return service.persons()
.map(this::personToSse)
.onErrorResume(e -> Mono.just(throwableToSse(e)));
}
private ServerSentEvent<Object> personToSse(Person person) {
return ServerSentEvent.builder().data(person).build();
}
Service:
public interface Service {
Flux<Person> persons();
}
I have two tests:
#SpringBootTest(classes = Config.class)
#AutoConfigureMockMvc
class PersonsControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private Service service;
#Test
void streamsPersons() throws Exception {
when(service.persons())
.thenReturn(Flux.just(new Person("John", "Smith"), new Person("Jane", "Doe")));
String responseText = mockMvc.perform(get("/persons").accept(MediaType.TEXT_EVENT_STREAM))
.andExpect(status().is2xxSuccessful())
.andExpect(content().string(not(isEmptyString())))
.andReturn()
.getResponse()
.getContentAsString();
assertThatJohnAndJaneAreReturned(responseText);
}
#Test
void handlesExceptionDuringStreaming() throws Exception {
when(service.persons())
.thenReturn(Flux.error(new RuntimeException("Oops!")));
String responseText = mockMvc.perform(get("/persons").accept(MediaType.TEXT_EVENT_STREAM))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
assertThat(responseText, is("event:internal-error\ndata:Oops!\n\n"));
}
First test checks that for the 'sunny day scenario' we get two persons that we expect. Second test checks what happens when an exception occurs.
The tests work perfectly when I run them one by one. But when I run them both, sometimes they pass, sometimes one of them fail, sometimes both fail. Failure reasons are different:
Sometimes Jackson complains during JSON parsing that an EOF was reached ('No content to map due to end-of-input', although in the log I can see a valid full JSON)
Sometimes first test fails and second passes as if in both cases an error was returned, even though I can see in the logs that for the first test normal response was generated, not the erroneous one
Sometimes second test fails and first passes as if in both cases valid JSONs where returned
So it looks like there is some concurrency problem. But my test code is simple enough, it does not use any concurrency-related concepts.
The following test fails 100% of times on my machine (it just runs these 2 tests repeatedly 1000 times):
#Test
void manyTimes() throws Exception {
for (int i = 0; i < 1000; i++) {
streamsPersons();
handlesExceptionDuringStreaming();
}
}
The questions follow:
Can MockMvc be used to test reactive controllers at all?
If it can, do I do anything incorrectly?
Here is the full project source code: https://github.com/rpuch/sse-webflux-tests
The manyTests() method is commented out and has to be re-enabled to be used.
1. Can MockMvc be used to test reactive controllers at all?
The answer is no, MockMvc is a blocking mockClient that will call your method once and return. It does not have the ability to read items consecutively as they get emitted The client you need to use is the Spring WebClient.
You can read more here how to go about testing infinite streams using the Spring WebTestClient.
2. If it can, do I do anything incorrectly?
See the answer to question one.
In my current project we are working on microservices(web app).
In unit tests we try to cover 85-90% of our code. I have noticed 2 approaches of testing using spring:
Inject a controller and invoke its methods directly
Form a proper request where you can specify cookies, headers... and then make a call
Moreover, we won't be able to test authentication with the 1 approach.
Which of the next spring testing ways should be used? And what are the (dis-)advantages of each type?
#RestController
class MyController {
#PostMapping(path="/path")
public String handle(#RequestBody MyRequest request) {
//service invoked
return "some value";
}
}
JUnit approach #1
#LotsOfAnnotations
class ControllerTest1 {
#Autowired
private MyController myController;
#Test
public String verboseNameTest() {
// Mock 3rd party calls
....
// Form request
MyRequest request = new MyRequest();
// Invoke testing method
myController.handle(request);
// Assert
}
}
JUnit approach #2
#LotsOfAnnotations
class ControllerTest2 {
#Autowired
private TestRestTemplate testTemplate;
private MockRestServiceServer server;
#Autowired
private MockMvc mockMvc;
#Autowired
private WebApplicationContext webAppContext;
#Before
public void setup() {
server = MockRestServiceServer.createServer(testTemplate.getRestTemplate());
mockMvc = MockMvcBuilders.webAppContextSetup(this.webAppContext).build();
}
#Test
public String verboseNameTest() {
// Mock 3rd party calls
....
// Form request
String jsonStringRequest = "{}";
RequestBuilder requestBuilder = MockMvcRequestBuilders
.post("/path")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonStringRequest);
// Make a call
MvcResult result = this.mockMvc
.perform(requestBuilder)
.andExpect(status().isOk())
.andReturn();
// Assert
}
}
As far as I can see you need to understand when to mock the service and when to make a direct call.
In your first approach you are directly calling the web service so let's say for example you have a huge code base and multiple developers are working at a time so you would get constant updates in the classes and if you want to test your piece of code(which is why you use JUnit) then you making direct call can result in error/ failed test case because someone might have changed something.
In second approach or mocking basically removes that possibility by mocking the other service which your code would be needing so that will make you code test easier.
But there are people who questions what's the use of mocking when you are not even testing the whole thing which is true up to certain point but the main reason for mocking is to test your piece of code only irrespective of other services that code is dependent on. Also if you talk about making actual service call that should be part of integration testing or end to end testing.