在Spring集成测试中模拟外部服务器

29

我有一个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是否导致了一些预期的结果。然而,我想在本地模拟外部服务器,这样我甚至不需要Internet访问就可以测试所有这些内容。最好的方法是什么?

我是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 Security对象中。但是我认为测试代码应该是一样的,因为我只是访问URL并期望响应,不是吗?


回复晚了,但为什么你可以使用WireMock进行集成测试呢?https://blog.avenuecode.com/how-to-use-wiremock-for-integration-testing - Kamlesh
4个回答

11

在尝试了各种不同的情境后,以下是一种方式可以最小干预主代码的情况下实现所要求的目标:

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

  2. @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";
        }
    }
    

'application.properties' 文件中的 'api_host' 等于 '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
    }
}

有没有更好的方法来完成这个?欢迎大家提供反馈!

P.S. 这里有一些我在将这个答案应用到更现实的应用程序中遇到的复杂情况:

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

  2. 如果您设置了Spring Security,则必须在安全配置中打开对模拟链接的访问权限。要附加到安全配置而不是搞乱主配置文件,请执行以下操作:

    @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。


7

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

2

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

要点是,

  • 使用Groovy DSL或其他支持显式调用/代理或记录的方法为外部服务创建合同。请查阅文档以确定适合您的内容。

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

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

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

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

一旦所有东西都运作正常,您将永远不会再回头使用 spring-cloud-contracts 进行所有测试。

@marcin-grzejszczak 作者也在SO上,并且他帮助很多人解决了这个问题。所以如果你遇到困难,只需在SO上发布。


0

你可以有另一个Spring配置文件,公开与HelloController类相同的端点。然后,您可以简单地返回预先定义好的JSON响应。

从您的代码中,我不确定您想要实现什么。如果您只是想查看对Facebook的调用是否有效,则没有任何替代方法可供测试实际与Facebook通信的服务。仅为确保正确模拟Facebook响应而模拟Facebook响应,对我来说并不是非常有用的测试。

如果您正在测试从Facebook返回的数据是否以某种方式发生了变化,并且您想确保对其进行的工作是正确的,则可以在单独的方法中执行该工作,该方法将Facebook响应作为参数,并进行变异。然后,您可以基于各种JSON输入进行检查,以确保它正常工作。

您可以在不涉及Web服务的情况下进行测试。


我使用这段代码来理解如何编写更复杂的集成测试。我想要测试并重构的实际系统由几个Spring安全模块组成,它们协同工作以提供与Facebook的oauth2。所有这些都是通过安全的无状态REST架构完成的,具有其自身的特点。最终,我想要测试这个复杂的系统 - 在Facebook调用之前和之后有几个步骤。这个控制器问题是我现在能想到的最简单明确的第一步,发布整个系统将使回答变得完全不可能。 - otognan
你想部分地测试你的应用程序在Spring Security身份验证错误时的行为,这样说是否公平? - Robert Moskal
我已经有一个可行的原型,但它看起来很丑。这个项目对我没有压力 - 所以我想做的是编写一些测试,以确保系统在我进行大规模重构时能够良好运行。是的,部分原因是我想测试身份验证拒绝和其他故障模式,但主要是为了确保在重构时保留工作功能。 - otognan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接