【问题标题】:Mock external server during integration testing with Spring在使用 Spring 进行集成测试期间模拟外部服务器
【发布时间】:2015-06-15 12:05:08
【问题描述】:

我有一个 Spring Web 服务器,它根据请求对某些第三方 Web API 进行外部调用(例如,检索 Facebook oauth 令牌)。从这个调用中获取数据后,它会计算一个响应:

@RestController
public class HelloController {
    @RequestMapping("/hello_to_facebook")
    public String hello_to_facebook() {
        // Ask facebook about something
        HttpGet httpget = new HttpGet(buildURI("https", "graph.facebook.com", "/oauth/access_token"));
        String response = httpClient.execute(httpget).getEntity().toString();
        // .. Do something with a response
        return response;
    }
}

我正在编写一个集成测试,检查在我的服务器上点击 url 会导致一些预期的结果。但是我想在本地模拟外部服务器,这样我什至不需要互联网访问来测试这一切。最好的方法是什么?

我是春天的新手,这就是我目前所拥有的。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest({})
public class TestHelloControllerIT {        
    @Test
    public void getHelloToFacebook() throws Exception {
        String url = new URL("http://localhost:8080/hello_to_facebook").toString();
        //Somehow setup facebook server mock ...
        //FaceBookServerMock facebookMock = ...

        RestTemplate template = new TestRestTemplate();
        ResponseEntity<String> response = template.getForEntity(url, String.class);
        assertThat(response.getBody(), equalTo("..."));

        //Assert that facebook mock got called
        //facebookMock.verify();
    }
}

实际的设置更复杂 - 我正在制作 Facebook oauth 登录,所有这些逻辑不在控制器中,而是在各种 Spring Security 对象中。但是我怀疑测试代码应该是相同的,因为我只是点击 url 并期望得到响应,不是吗?

【问题讨论】:

    标签: spring spring-security spring-boot integration-testing


    【解决方案1】:

    在尝试了各种场景之后,这是一种如何在对主代码干预最少的情况下实现所要求的内容的一种方法

    1. 重构您的控制器以使用第三方服务器地址的参数:

      @RestController
      public class HelloController {
          @Value("${api_host}")
          private String apiHost;
      
          @RequestMapping("/hello_to_facebook")
          public String hello_to_facebook() {
              // Ask facebook about something
              HttpGet httpget = new HttpGet(buildURI("http", this.apiHost, "/oauth/access_token"));
              String response = httpClient.execute(httpget).getEntity().toString();
              // .. Do something with a response
              return response + "_PROCESSED";
          }
      }
      

    'api_host' 等于 src/main/resources 中 application.properties 中的 'graph.facebook.com'

    1. 在 src/test/java 文件夹中创建一个模拟第三方服务器的新控制器。

    2. 覆盖 'api_host' 以测试到 'localhost'。

    为简洁起见,以下是第 2 步和第 3 步的代码:

    @RestController
    class FacebookMockController {
        @RequestMapping("/oauth/access_token")
        public String oauthToken() {
            return "TEST_TOKEN";
        }
    }
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    @IntegrationTest({"api_host=localhost",})
    public class TestHelloControllerIT {        
        @Test
        public void getHelloToFacebook() throws Exception {
            String url = new URL("http://localhost:8080/hello_to_facebook").toString();
            RestTemplate template = new TestRestTemplate();
            ResponseEntity<String> response = template.getForEntity(url, String.class);
            assertThat(response.getBody(), equalTo("TEST_TOKEN_PROCESSED"));
    
            // Assert that facebook mock got called:
            // for example add flag to mock, get the mock bean, check the flag
        }
    }
    

    有没有更好的方法来做到这一点?感谢所有反馈!

    附:以下是将这个答案放入更现实的应用程序时遇到的一些复杂情况:

    1. Eclipse 将测试和主配置混合到类路径中,因此您可能会通过测试类和参数搞砸主配置:https://issuetracker.springsource.com/browse/STS-3882 使用 gradle bootRun 来避免它

    2. 如果您设置了 spring 安全性,则必须在安全性配置中打开对模拟链接的访问权限。要附加到安全配置而不是弄乱主配置配置:

      @Configuration
      @Order(1)
      class TestWebSecurityConfig extends WebSecurityConfig {
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http
                  .authorizeRequests()
                      .antMatchers("/oauth/access_token").permitAll();
              super.configure(http);
          }
      }
      
    3. 在集成测试中点击 https 链接并不简单。我最终使用了带有自定义请求工厂和配置 SSLConnectionSocketFactory 的 TestRestTemplate。

    【讨论】:

      【解决方案2】:

      如果您在 HelloController 中使用 RestTemplate,您将能够对其进行 MockRestServiceTest 测试,如下所示:https://www.baeldung.com/spring-mock-rest-template#using-spring-test

      在这种情况下

      @RunWith(SpringJUnit4ClassRunner.class)
      // Importand we need a working environment
      @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
      public class TestHelloControllerIT {    
      
          @Autowired
          private RestTemplate restTemplate;
      
          // Available by default in SpringBootTest env
          @Autowired
          private TestRestTemplate testRestTemplate;
      
          @Value("${api_host}")
          private String apiHost;
      
          private MockRestServiceServer mockServer;
      
          @Before
          public void init(){
              mockServer = MockRestServiceServer.createServer(this.restTemplate);
          }
      
          @Test
          public void getHelloToFacebook() throws Exception {
      
              mockServer.expect(ExpectedCount.manyTimes(),
                  requestTo(buildURI("http", this.apiHost, "/oauth/access_token"))))
                  .andExpect(method(HttpMethod.POST))
                  .andRespond(withStatus(HttpStatus.OK)
                          .contentType(MediaType.APPLICATION_JSON)
                          .body("{\"token\": \"TEST_TOKEN\"}")
                  );
      
              // You can use relative URI thanks to TestRestTemplate
              ResponseEntity<String> response = testRestTemplate.getForEntity("/hello_to_facebook", String.class);
              // Do the test you need
          }
      }
      

      请记住,您需要一个通用的 RestTemplateConfiguration 来进行自动装配,如下所示:

      @Configuration
      public class RestTemplateConfiguration {
      
          /**
           * A RestTemplate that compresses requests.
           *
           * @return RestTemplate
           */
          @Bean
          public RestTemplate getRestTemplate() {
              return new RestTemplate();
          }
      }
      

      而且你也必须在 HelloController 中使用它

      @RestController
      public class HelloController {
      
          @Autowired
          private RestTemplate restTemplate;
      
          @RequestMapping("/hello_to_facebook")
          public String hello_to_facebook() {
      
              String response = restTemplate.getForEntity(buildURI("https", "graph.facebook.com", "/oauth/access_token"), String.class).getBody();
              // .. Do something with a response
              return response;
          }
      }
      

      【讨论】:

        【解决方案3】:

        2018 年情况有了很大改善。 我最终使用了spring-cloud-contracts 这是视频介绍https://www.youtube.com/watch?v=JEmpIDiX7LU。演讲的第一部分将向您介绍遗留服务。那就是您可以用于外部 API 的那个。

        要点是,

        • 您可以使用 Groovy DSL 或其他甚至支持显式调用/代理或录制的方法为外部服务创建契约。查看文档以了解适合您的方法

        • 由于在这种情况下您实际上无法控制第 3 方,因此您将使用 contract-verifier 并在本地创建存根,但请记住 skipTests

        • stub-jar 现在已编译并可用,您可以在测试用例中运行它,因为它会为您运行 Wiremock。

        这个问题和几个 stackoverflow 答案帮助我找到了解决方案,所以这是我的示例项目,供下一个进行这些和其他类似微服务相关测试的人使用。

        https://github.com/abshkd/spring-cloud-sample-games

        一旦一切正常,您将永远不会回头并使用 spring-cloud-contracts 进行所有测试

        @marcin-grzejszczak 作者也在 SO 上,他帮助解决了这个问题。因此,如果您遇到困难,请在 SO 上发帖。

        【讨论】:

          【解决方案4】:

          您可以有另一个 spring 配置文件,该文件公开与 HelloController 类相同的端点。然后,您可以简单地返回预设的 json 响应。

          根据您的代码,我不确定您要完成什么。如果您只是想查看对 facebook 的调用是否有效,那么无法替代对实际与 facebook 对话的服务进行测试。模拟 facebook 响应只是为了确保它被正确地模拟,并没有让我觉得这是一个非常有用的测试。

          如果您正在测试以查看从 facebook 返回的数据以某种方式发生了变异,并且您想确保对其所做的工作是正确的,那么您可以使用单独的方法来完成这项工作将 facebook 响应作为参数,然后进行突变。然后,您可以根据各种 json 输入检查它是否正常工作。

          您可以在完全不使用 Web 服务的情况下进行测试。

          【讨论】:

          • 我用这段代码来了解如何编写更复杂的集成测试。我要测试然后重构的实际系统由几个 Spring 安全模块组成,这些模块与 facebook 一起提供 oauth2。所有这些都是通过安全的无状态 REST 架构完成的,它有自己的特点。最终我想测试这个复杂的系统——在 facebook 调用之前有几个步骤,之后有几个步骤。这个控制器问题是我现在能想到的最简单清晰的第一步,我发布整个系统将使它完全无法回答。
          • 公平地说,当 Spring Security 出现身份验证错误时,您希望部分测试应用程序的行为?
          • 我已经有一个可以工作的原型,但它很丑。我对这个项目没有压力——所以我想做的是编写一些测试,以确保系统运行良好,同时我将进行大的重构。所以是的,部分我想要测试身份验证拒绝和其他失败模式,但主要是我想确保我在重构时保留了工作功能。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2022-01-20
          • 1970-01-01
          • 1970-01-01
          • 2021-03-15
          • 1970-01-01
          • 1970-01-01
          • 2015-08-23
          相关资源
          最近更新 更多