【问题标题】:How to mock REST server before Spring context initialization?如何在 Spring 上下文初始化之前模拟 REST 服务器?
【发布时间】:2018-04-05 11:10:21
【问题描述】:

我有一个 Spring Bean,它使用 RestTemplateBuilder 在其构造函数中发送请求以发送请求:

@Service
class MyService {

  MySettingsFromRemote settings;

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

在测试期间,我想使用一些预定义的数据来模拟使用 MockRestServiceServer(或者可能模拟用于发送请求的 RestTemplateBuilder)的响应,以便加载应用程序上下文。地址写在application.properties文件中。

我试过这样做:

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() {
        
    }
}

但上下文在 @Before 方法执行之前加载并失败,并显示一条消息称 MockRestServiceServer 没有预期请求。

然后我尝试使用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));
    }
}

但随后它抱怨MockServerRestTemplateCustomizer has not been bound to a RestTemplate.我认为可以通过在测试类上使用@RestClientTest 注释来解决此问题,因为它会禁用RestTemplateBuilder 的自动配置并启用MockRestServiceServer 的确认:

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

但它并没有改变任何东西。

提前致谢。

【问题讨论】:

    标签: java spring unit-testing junit


    【解决方案1】:

    这个设置对我有用:

    @Service
    public class MyService implements ApplicationListener<ContextRefreshedEvent> {
    
        private final RestTemplate restTemplate;
    
        public MyService(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
        }
    
        @Override
        public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
            //make your call here
            restTemplate.getForEntity("http://www.localhost:9090", String.class);
        }
    
    }
    

    当且仅当 spring 上下文完全初始化时才会调用

    在测试类中:

    @TestConfiguration
    public class MockServiceCallConfiguration {
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Bean
        public MockRestServiceServer mockRestServiceServer() {
            MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
    
            server.expect(MockRestRequestMatchers.requestTo("http://www.localhost:9090"))
                    .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
    
            return server;
        }
    }
    
    @SpringBootTest(classes = {MockServiceCallConfiguration.class})
    @RunWith(SpringRunner.class)
    public class MyServiceTest {
    
    
        @Test
        public void test() {
    
        }
    
    }
    

    这很乏味,如果您可以重构代码并避免在初始化设置期间调用rest服务而是在某些业务事件中调用,那肯定会更好

    【讨论】:

    • 我明天会尝试并提供更新,感谢您的想法!
    • @Sam 没问题告诉我
    • 不确定会发生什么变化。 @PostConstruct 在构造函数之后立即执行,所以绝对没有任何变化
    • @Sam 抱歉,我似乎误解了您的用例,我发布了一个新的解决方案。我看到你找到了自己的,选择最适合你的
    • 感谢您更新答案!我无法重构代码,因为应用程序必须通过 HTTP 从另一个组件获取一些设置才能运行,因此必须将其置于启动时。另外,你使用的是RestTemplate,而我使用的是RestTemplateBuilder,所以这有点不同。我会考虑将请求移动到监听器方法,谢谢
    【解决方案2】:

    我找到了一种方法(如果有人有更好的解决方案,请留下另一个答案)。 @RestClientTest 做了一个非常接近在这种情况下需要的,但还不够。我们需要RestTemplateBuilder 始终生成相同的RestTemplate,我们还需要绑定到RestTemplateMockRestServiceServer。我通过用模拟替换 RestTemplateBuilder Bean 定义来做到这一点,并在 bean 定义中预配置了 MockRestServiceServer 以确保在发送任何请求之前执行此代码。

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.context.TestConfiguration;
    import org.springframework.boot.web.client.RestTemplateBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Primary;
    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 org.springframework.web.client.RestTemplate;
    
    import static org.mockito.Mockito.mock;
    import static org.mockito.Mockito.when;
    import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class CollectorApplicationTest {
    
        @Test
        public void contextLoads() {
    
        }
    
        @TestConfiguration
        static class A {
    
            @Primary
            @Bean
            public RestTemplateBuilder restTemplateBuilder(@Value("${components.web-admin-portal.rest.schemas}")
                                                                   String webAdminPortal) {
    
                RestTemplate          restTemplate = new RestTemplate();
                MockRestServiceServer server       = MockRestServiceServer.createServer(restTemplate);
                server.expect(MockRestRequestMatchers.requestTo(webAdminPortal))
                      .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
    
                RestTemplateBuilder mockBuilder = mock(RestTemplateBuilder.class);
                when(mockBuilder.build()).thenReturn(restTemplate);
                return mockBuilder;
            }
        }
    }
    

    这样应用就成功启动了。

    【讨论】:

      【解决方案3】:

      看看我 2021 年的老问题,我现在会做不同的事情。

      看来我需要像这样从远程服务器获取一些设置(下面的伪代码):

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

      所以,我不这样做,而是像这样重新组织代码:

      @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);
        }
      }
      

      那么就很容易测试了。只需提供@MockBeanMySettingsFromRemote

      【讨论】:

        猜你喜欢
        • 2012-07-21
        • 2020-08-22
        • 2011-08-16
        • 1970-01-01
        • 1970-01-01
        • 2014-06-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多