Search code examples
javaspringunit-testingjunit

How to mock REST server before Spring context initialization?


I have a Spring Bean that sends a request inside its constructor using RestTemplateBuilder in order to send a request:

@Service
class MyService {

  MySettingsFromRemote settings;

  MyService(RestTemplateBuilder builder, @Value("${my-url}") String url){
    var rt = builder.build();
    setting = rt.getForEntity(url, MySettingsFromRemote.class);
  }
  
  ...
}

During testing, I would like to mock the response using MockRestServiceServer (or maybe mock the RestTemplateBuilder that is used to send the request) with some predefined data just so the application context loads. The address is written in the application.properties file.

I tried doing this:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureMockRestServiceServer;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.match.MockRestRequestMatchers;

import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

@RunWith(SpringRunner.class)
@AutoConfigureMockRestServiceServer
@SpringBootTest
public class CollectorApplicationTest {
    
    @Autowired
    MockRestServiceServer server;
    
    @Value("${components.web-admin-portal.rest.schemas}")
    String webAdminPortal;
    
    @Before
    public void init() {
        
        server.expect(MockRestRequestMatchers.requestTo(webAdminPortal))
              .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
    }
    
    @Test
    public void contextLoads() {
        
    }
}

But the context loads before the @Before method is executed and fails with a message saying the MockRestServiceServer didn't expect the request.

Then I tried using ApplicationContextInitializer:

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.match.MockRestRequestMatchers;

import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

public class AppInit implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    
    @Override
    public void initialize(final ConfigurableApplicationContext context) {
        
        context.refresh();
        MockRestServiceServer server         = context.getBean(MockRestServiceServer.class);
        String                webAdminPortal = context.getEnvironment()
                                                      .getProperty("components.web-admin-portal.rest.schemas");
        
        server.expect(MockRestRequestMatchers.requestTo(webAdminPortal))
              .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
    }
}

But then it complains that the MockServerRestTemplateCustomizer has not been bound to a RestTemplate. I figured that this issue may be resoled by using @RestClientTest annotation on the test class since it would disable auto configuration of RestTemplateBuilder and enable confiration of MockRestServiceServer:

@RunWith(SpringRunner.class)
@SpringBootTest
@RestClientTest
@ContextConfiguration(initializers = AppInit.class)
public class CollectorApplicationTest {

But it did not change anything.

Thanks in advance.


Solution

  • Looking at my old question in 2021, I'd do it differently now.

    It seems I needed to get some settings from a remote server like this (pseudo code below):

    @Service
    class MyService {
    
      MySettingsFromRemote settings;
    
      MyService(RestTemplateBuilder builder, @Value("${my-url}") String url){
        var rt = builder.build();
        setting = rt.getForEntity(url, MySettingsFromRemote.class);
      }
      
      ...
    }
    

    So, instead of doing that, I'd reorganize the code like this:

    @Service
    class MyService {
    
      MySettingsFromRemote settings;
    
      MyService(MySettingsFromRemote settings){
        this.settings = settings;
      }
      
      ...
    }
    
    
    @Configuration
    class MyConfig {
    
      // maybe Spring provides this bean now?
      // rest template is kinda outdated already, webclient is preferred.
      @Bean RestTemplate myRestTemplate(RestTemplateBuilder builder) {
        return builder.build();
      }
    
      @Bean MySettingsFromRemote mySettingsFromRemote(RestTemplate restTemplate, @Value("${my-url}") String url){
        return restTemplate.getForEntity(url, MySettingsFromRemote.class);
      }
    }
    

    Then it's very easy to test. Just need to provide a @MockBean of MySettingsFromRemote.