javaspring-bootunit-testingmockitospring-rest

Unable to mock a RestClient bean when testing a class that uses this bean


So I have two classes : A RestClient class and another MetadataService class which uses the restClient bean.

This is the RestClient class

@Configuration
public class RestClientConfig {
  //actual class is more complex than this as we are authenticating with kerberos and there's
  //code that generates kerberos token and injects it into the rest client builder 
  @Bean("customRestClient")
  public RestClient restClient(RestClient.Builder builder) {
    return builder.build();
  }
}
 

Below is the service class

@Component
public class MetadataService {
  @Autowired 
  @Qualifier("customRestClient")
  RestClient customRestClient;
  
  public void makeCall() {
     try {
         final String response = customRestClient.method(HTTPMethod.POST).uri("http://example.com")
                                      .headers(getHeaders()).body(//some body here).retrieve()
                                      .toEntity(String.class);
         log.info(response);
     }
  }
}


Now I want to write a test class for MetadataService.

Things I have tried :

  1. Using @Mock annotation on the restClient bean but that lead to exceptions (more below)
  2. Using @RestClientTest on the class albeit there's a lot of confusion on how I should use this.

Here is my test class which I have written so far (what I have referenced in #1)

public class MetadataServiceTest {

@Mock
RestClient customRestClient;

@InjectMock
MetadataService metadataService;

@Test
public void testMakeCall() {

when(customRestClient.method(HTTPMethod.POST).uri("http://example.com")
     .headers(any()).body(any()).retrieve()
     .toEntity(String.class)).thenReturn(response); 

metadataService.makeCall();
}

}

This leads to exceptions like this one:

java.lang.NullPointerException: Cannot invoke "org.springframework.web.client.RestClient$RequestBodyUriSpec(String, Object[])" because the return value of "org.springframework.web.client.RestClient.method(org.springframework.http.HttpMethod)" is null


Solution

  • You are missing the @ExtendWith(MockitoExtension.class) annotation to have your test run with Mockito support. Without the annotation, @Mock and friends are completely ignored.

    Additionally, you would only be stubbing the first level of calls on customRestClient. Without existing stubs, a Mockito mock object will return the default value for any given type (0 for numeric types, false for booleans, empty collections, null for objects and strings). Writing something like when(mock.method()).thenReturn(result) still must call the method method on the mock instance. There's no way around it and Mockito cannot change the behavior of the Java language.

    This means that a line such as

    when(customRestClient.method(HTTPMethod.POST).uri("http://example.com")
         .headers(any()).body(any()).retrieve()
         .toEntity(String.class)).thenReturn(response); 
    

    will first call customRestClient.method(HTTPMethod.POST), then call uri("http://example") on its result. Since customRestClient.method is not stubbed, it returns the default value (i.e. null). You cannot call a method on a null reference and consequently, you encounter a NullPointerException. It just so happens that when ignores the value passed in and does not do anything with the return value from the method call; only the return type is inferred from the expression.

    You would have to stub each level of calls. First stub customRestClient.method to return a mock object. Then stub the uri call on this mock object to return another mock object. Stub headers on that object and so on.

    Fortunately, Mockito provides a helpful shortcut: @Mock(Answers.RETURNS_DEEP_STUBS). This sets up the mock object to return another mock object from each of its methods. Subsequently, customRestClient.method(HTTPMethod.POST) in your when call returns a mock and calling uri("http://example.com") on this mock will return another mock, etc. Eventually, the toEntity call on the final mock object returned from your call chain is stubbed to return response.

    Simply add it as attribute on the annotation:

    @Mock(RETURNS_DEEP_STUBS)
    RestClient customRestClient;
    

    Just be aware of the JavaDoc of RETURNS_DEEP_STUBS:

    WARNING: This feature should rarely be required for regular clean code! Leave it for legacy code. Mocking a mock to return a mock, to return a mock, (...), to return something meaningful hints at violation of Law of Demeter or mocking a value object (a well known anti-pattern).

    Good quote I've seen one day on the web: every time a mock returns a mock a fairy dies.