Search code examples
springspring-bootspring-mvcspring-boot-testspring-restclient

Spring Boot RestClient as a singleton or create new instances per request


Looking through the documentation here, the expected pattern to use for RestClient is to autowire the RestClient.Builder and then build the RestClient in the constructor method of a service. While this works great when using in a regular class, it does not work as well when testing.

According to the documentation here, the @RestClientTest and @AutoConfigureMockRestServiceServer are usable when there is only 1 builder used within your application. As more instances are created and specific customization is needed, it becomes apparent that you will want to have more than one RestClient.Builder for each specific service.

With this, the only way that I can see to configure it is to bind the MockRestServiceServer to each RestClient.Builder making having them as beans seem to be the obvious route. Now you can have some code like the following:

Configuration logic:

@RequiredArgsConstructor
@Configuration
public class RestClientConfiguration {
    private final RestClientBuilderConfigurer restClientBuilderConfigurer;

    @Bean
    RestClient.Builder microservice1RestClientBuilder() {
        return createDefaultRestClientBuilder()
                .baseUrl("http://localhost:8081");
    }

    /**
     * Copied from
     * {@link org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
     * RestClientAutoConfiguration}
     * 
     * @return
     */
    private RestClient.Builder createDefaultRestClientBuilder() {
        RestClient.Builder builder = RestClient
                .builder()
                .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS));
        return restClientBuilderConfigurer.configure(builder);
    }
}

Controller logic:

@RequiredArgsConstructor
@RestController
public class MainController {
    private final MainService mainService;

    @GetMapping
    public String hello() {
        return mainService.helloService();
    }
}

Service logic:

@Service
public class MainService {
    private final RestClient restClient;
    
    public MainService(RestClient.Builder microservice1RestClientBuilder) {
        super();
        restClient = microservice1RestClientBuilder.build();
    }

    public String helloService() {
        return restClient.get().uri("/").retrieve().body(String.class);
    }
}

Test logic:

@SpringBootTest
@AutoConfigureMockMvc
class MainControllerTest {
    private MockRestServiceServer microservice1Server;

    @Autowired
    private RestClient.Builder microservice1RestClientBuilder;

    @Autowired
    private MockMvc mvc;

    @BeforeEach
    void init() {
        microservice1Server = MockRestServiceServer.bindTo(microservice1RestClientBuilder).build();
    }

    @Test
    void test() throws Exception {
        microservice1Server
                .expect(requestTo("http://localhost:8081/"))
                .andRespond(withSuccess("Hello world", MediaType.TEXT_PLAIN));
        mvc.perform(get("/")).andExpect(status().is2xxSuccessful()).andExpect(content().string("Hello world"));
    }
}

This all seems like it should work, but it does not because the test will not bind before the RestClient is created. To fix this, the service class can be changed to the following and the test will begin to work.

@RequiredArgsConstructor
@Service
public class MainService {
    private final RestClient.Builder microservice1RestClientBuilder;

    public String helloService() {
        return microservice1RestClientBuilder.build().get().uri("/").retrieve().body(String.class);
    }
}

The test case works now however the RestClient is now created per call which feels unnecessary. This makes me believe either I am missing how it is expected to be used or there is a gap somewhere. My hope with this question is to show the optimal way to use RestClient not only for general usage but also for testing purposes.


Solution

  • You are correct that @AutoConfigureMockRestServiceServer will not work in cases like this. It looks like you will have the best luck using MockServerRestClientCustomizer. The javadoc for the class shows an example of its usage, and there is an integration test case that can also be used as an example.

    With this technique, your test class might look something like this:

    @SpringBootTest
    @AutoConfigureMockMvc
    class MainControllerTest {
    
        @Autowired
        private MainService mainService;
    
        @Autowired
        private MockServerRestClientCustomizer customizer;
    
        @Autowired
        private MockMvc mvc;
    
        @TestConfiguration
        public static class MainControllerTestConfiguration {
            @Bean
            MockServerRestClientCustomizer mockServerRestClientCustomizer() {
                return new MockServerRestClientCustomizer();
            }
        }
    
        @Test
        void test() throws Exception {
            this.customizer.getServer(this.mainService.getRestClientBuilder())
                    .expect(requestTo("http://localhost:8081/"))
                    .andRespond(withSuccess("Hello world", MediaType.TEXT_PLAIN));
    
            mvc.perform(get("/"))
                .andExpect(status().is2xxSuccessful())
                .andExpect(content().string("Hello world"));
        }
    }