Home > other >  How to mock Spring @Autowired WebClient response in @Service?
How to mock Spring @Autowired WebClient response in @Service?

Time:07-07

I'd like to test program behavior when a @Service class that uses an @Autowired WebClient retrieves different response JSONs. To do so I'd like, in the tests, to be able to replace the response body JSON retrieved from the api url with a JSON read from a file.

Specifically I'd like to test the validations done in the DTO with the use of @NotNull and @Size annotations (when the JSON is not valid) and the behavior of the classes that uses the @Autowired ModelService when a different (valid) model mapped from the JSON is retrieved with the method .getModel().

My service look like this:

@Service
public class ModelServiceImpl implements ModelService {

   @Autowired
   ApiPropertiesConfig apiProperties;

   @Autowired
   private WebClient webClient;

   private static final ModelMapper modelMapper = Mappers.getMapper(ModelMapper.class);

   public Mono<Model> getModel() throws ConfigurationException {
   
       String apiUrl = apiProperties.getApiUrl();

       return webClient.get()
               .uri(apiUrl)
               .accept(MediaType.APPLICATION_JSON)
               .retrieve()
               .bodyToMono(ModelDTO.class)
               .map(modelMapper::modelDTOtoModel);
   }
}

My WebClient is defined as:

@Configuration
@EnableWebFlux
public class WebFluxConfig implements WebFluxConfigurer {

   @Bean
   public WebClient getWebClient() {
       HttpClient httpClient = HttpClient.create()
               .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
               .doOnConnected(conn -> conn
                       .addHandlerLast(new ReadTimeoutHandler(10))
                       .addHandlerLast(new WriteTimeoutHandler(10)));

       ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient.wiretap(true));

       return WebClient.builder()
               .clientConnector(connector)
               .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
               .build();
   }
}

The ApiPropertiesConfig is:

@Configuration
@ConfigurationProperties(prefix = "api")
@Data
@Primary
public class ApiPropertiesConfig {
   private String apiUrl;
}

I've setup the test class as:

@SpringBootTest
@TestPropertySource(properties = {
       "api.apiUrl=https://url.to.production.model/model.json"
})
@ExtendWith(MockitoExtension.class)
class ApplicationTests {

}

As you can see, when i call modelSerice.getModel() the webclient retrieves a json from an api url, converts it to a DTO that is then mapped to a POJO using a Mapstruct interface.

I've read the options suggested here: How to mock Spring WebFlux WebClient?, but I wasn't able to understand how to "replace", in the service, the autowired WebClient with mocked one, during the tests.

CodePudding user response:

Your problem stems from the fact that you are using field injection instead of the best practice which is to use constructor based injection. Aside from the fact that the former produces -- as you have noticed -- code that is hard to test, it has other drawbacks as well. For example:

  1. It disallows creating immutable components (i.e. fields are not final)
  2. It may lead to code that violates the Single Responsibility Principle (as you may easily inject a large number of dependencies that may go unoticed)
  3. It couples your code directly to the DI container (i.e. you cannot use your code outside of the DI container easily)
  4. It may hide injected dependencies (i.e. there is no clear differentiation between optional and required dependencies).

Based on all that, it would be better if you used constructor injection to do you job. With that in place injecting mocks into services can be done as easy as:


@Mock // The mock we need to inject
private MyMockedService mockedService;

@InjectMocks // The unit we need to test and inject mocks to
private MyUnitToTestServiceImpl unitToTestService;

Alternatively you can use instantiate the unit to be tested directly and simply pass in the mocked instance through its public constructor.

Thus the rule of thumb would be to always use constructor injection for such cases.

CodePudding user response:

Since you use @SpringBootTest annotation which enhances your tests with SpringExtension you can use @MockBean to inject the mock bean into the application context and replace the existing one:

@SpringBootTest
@TestPropertySource(properties = {
       "api.apiUrl=https://url.to.production.model/model.json"
})
class ApplicationTests {
    
    @MockBean
    private WebClient webClient;
    
    // use the usual Mockito mocking on `webClient` like doReturn(...).when(webClient).get(...);
}
  • Related