在使用 Spring 进行集成测试期间模拟外部服务器

Mock external server during integration testing with Spring

我有一个 Spring 网络服务器,它根据请求对某些第三方网络进行外部调用 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 是否会产生一些预期的结果。但是我想在本地模拟外部服务器,这样我什至不需要互联网访问来测试这一切。最好的方法是什么?

我是spring的新手,这是我目前的情况。

@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 安全对象中。但是我怀疑测试代码应该是相同的,因为我只是点击 urls 并期待响应,不是吗?

您可以有另一个 spring 配置文件,它公开与 HelloController class 相同的端点。然后,您可以简单地 return 罐头 json 响应。

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

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

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

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

  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' 等于 'graph.facebook.com' 在 application.properties 在 src/main/resources

  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
    }
}

有更好的方法吗?感谢所有反馈!

P.S。以下是我将此答案放入更现实的应用程序时遇到的一些复杂问题:

  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 链接并不简单。我最终将 TestRestTemplate 与自定义请求工厂一起使用并配置了 SSLConnectionSocketFactory.

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

要点是,

  • 您使用 Groovy DSL 或其他甚至支持显式 calls/proxy 或记录的方法为外部服务创建合同。查看适合您的文档

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

  • 随着 stub-jar 现已编译并可用,您可以 运行 在您的测试用例中使用它,因为它将 运行 为您提供 Wiremock。

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

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

一旦一切正常,您将永远不会回头用 spring-cloud-contracts

进行所有测试

@marcin-grzejszczak 作者也在 SO 上,他帮助解决了很多问题。因此,如果您遇到困难,只需 post 就可以了。

如果您在 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;
    }
}