简体   繁体   中英

Mocking RestTemplate#postForObject in a unit test

Given a class EncoderService which has the following createNewStream method and a bunch of constants used in the method, how can I use mockito to write a unit-test for the createNewStream method:

public ResponseEntity<Object> createNewStream(Long channelId) {
    String url = IP + VERSION + serverName + VHOSTS + vhostName + APP_NAME + appName + STREAM_FILES;

    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON_UTF8));
    headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
    headers.setAcceptCharset(Arrays.asList(Charset.forName(UTF_8)));

    RestTemplate restTemplate = new RestTemplate();

    String udp = "udp://" + "localhost" + ":" + "1935";
    Map<String, String> map = new HashMap<>();
    map.put("name", STREAMS + appName + channelId);
    map.put("serverName", serverName);
    map.put("uri", udp);
    HttpEntity<Map<String, String>> request = new HttpEntity<>(map, headers);

    HttpStatus statusCode = null;
    try {
        ResponseEntity<Object> response = restTemplate.postForEntity(url, request, Object.class);
        statusCode = response.getStatusCode();
        map.put(MESSAGE, "successful");
        return new ResponseEntity<>(map, statusCode);
    } catch (HttpStatusCodeException e) {
        map.put(MESSAGE, e.getMessage());
        return new ResponseEntity<>(map, HttpStatus.BAD_REQUEST);
    }
}

RestTemplate is a class, not an interface, and it implements the actual HTTP transport. Both are standing in the way of writing a testable method. On top of that the fact that you are constructing an instance of a class that has side effects on the OS level rather than getting it injected does not help the case. So the way to solve it is:

  • write your method based around an interface rather than an implementation, RestOperations in this case
  • inject an instance implementing RestOperations , eg an instance of RestTemplate for production, via a constructor argument (preferred), method argument or via a Supplier<RestOperations> defined as a field on the class
  • substitute an actual instance with a test implementation or a mock in test. I guess it is easier to go for a Mockito.mock(RestOperations.class) because RestOperations just like all other Spring interfaces defines way too many method for writing a test implementation manually

So in EncoderService you can have:

private final RestOperations restClient;

public EncoderService(RestOperations restClient) {
  this.restClient = restClient;
}

public ResponseEntity<Object> createNewStream(Long channelId) {
  ...
  ResponseEntity<Object> response = restClient.postForEntity(...
  ...
}

And then in EncoderServiceTest :

ResponseEntity<Object> expectedReturnValue = ...

RestOperations testClient = mock(RestOperations.class);
doReturn(expectedReturnValue).when(testClient).postForEntity(any(), any(), anyClass());

EncoderService service = new EncoderService(testClient);
// use the service

For the other two cases the test setup is exactly the same, just you would pass the instance into the method call instead of constructor or overwrite the supplier on the EncoderService instance to return the testClient .

I have answered a very similar question about ProcessBuilder which also has side effects on the OS level and was constructed directly in the method under test here Error trying to mock constructor for ProcessBuilder using PowerMockito You can apply exactly the same tactics.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM