Java 如何使用 spring-cloud-netflix 和 feign 编写集成测试

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/39465990/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 21:10:30  来源:igfitidea点击:

How to write integration tests with spring-cloud-netflix and feign

javanetflix-eurekaspring-cloud-netflixwiremockfeign

提问by Bastian Voigt

I use Spring-Cloud-Netflix for communication between micro services. Let's say I have two services, Foo and Bar, and Foo consumes one of Bar's REST endpoints. I use an interface annotated with @FeignClient:

我使用 Spring-Cloud-Netflix 进行微服务之间的通信。假设我有两个服务,Foo 和 Bar,Foo 使用 Bar 的 REST 端点之一。我使用一个带有注释的接口@FeignClient

@FeignClient
public interface BarClient {
  @RequestMapping(value = "/some/url", method = "POST")
  void bazzle(@RequestBody BazzleRequest);
}

Then I have a service class SomeServicein Foo, which calls the BarClient.

然后我SomeService在 Foo 中有一个服务类,它调用BarClient.

@Component
public class SomeService {
    @Autowired
    BarClient barClient;

    public String doSomething() {
      try {
        barClient.bazzle(new BazzleRequest(...));
        return "so bazzle my eyes dazzle";
      } catch(FeignException e) {
        return "Not bazzle today!";
      }

    }
}

Now, to make sure the communication between services works, I want to build a test that fires a real HTTP request against a fake Bar server, using something like WireMock. The test should make sure that feign correctly decodes the service response and reports it to SomeService.

现在,为了确保服务之间的通信有效,我想构建一个测试,使用 WireMock 之类的东西,针对假 Bar 服务器发出真实的 HTTP 请求。该测试应确保 feign 正确解码服务响应并将其报告给SomeService.

public class SomeServiceIntegrationTest {

    @Autowired SomeService someService;

    @Test
    public void shouldSucceed() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(204);

      String result = someService.doSomething();

      assertThat(result, is("so bazzle my eyes dazzle"));
    }

    @Test
    public void shouldFail() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(404);

      String result = someService.doSomething();

      assertThat(result, is("Not bazzle today!"));
    }
}

How can I inject such a WireMock server into eureka, so that feign is able to find it and communicate with it? What kind of annotation magic do I need?

如何将这样的 WireMock 服务器注入 eureka,以便 feign 能够找到它并与之通信?我需要什么样的注释魔法?

采纳答案by Bastian Voigt

Use Spring's RestTemplate instead of feign. RestTemplate is also able to resolve service names via eureka, so you can do something like this:

使用 Spring 的 RestTemplate 而不是 feign。RestTemplate 还可以通过 eureka 解析服务名称,因此您可以执行以下操作:

@Component
public class SomeService {
   @Autowired
   RestTemplate restTemplate;

   public String doSomething() {
     try {
       restTemplate.postForEntity("http://my-service/some/url", 
                                  new BazzleRequest(...), 
                                  Void.class);
       return "so bazzle my eyes dazzle";
     } catch(HttpStatusCodeException e) {
       return "Not bazzle today!";
     }
   }
}

This is way easier testable with Wiremock than feign.

使用 Wiremock 比 feign 更容易测试。

回答by Sergey Bespalov

Probably there is no way to make WireMock comunicate directly with Eureka Server, but you can use other variants to configure test environment that you need.

可能没有办法让 WireMock 直接与 Eureka Server 通信,但是您可以使用其他变体来配置您需要的测试环境。

  1. In test environment you can deploy Eureka Service Registry under standalone Jetty servlet container and all the annotations will work like they do in real production environment.
  2. If you don't want to use real BarClientendpoint logic, and integration test is only about real httptransport layer, then you can use Mockito for BarClientendpoint stub.
  1. 在测试环境中,您可以在独立的 Jetty servlet 容器下部署 Eureka Service Registry,并且所有注释都将像在实际生产环境中一样工作。
  2. 如果您不想使用真正的BarClient端点逻辑,并且集成测试只是关于真正的http传输层,那么您可以使用 Mockito 来进行BarClient端点存根。

I suppose that in order to implement 1 and 2 using Spring-Boot, you will need to make two separate applications for a test environment. One for Eureka Service Registry under Jetty and another for BarClientendpoint stub under Jetty too.

我想为了使用 Spring-Boot 实现 1 和 2,您需要为测试环境制作两个单独的应用程序。一个用于 Jetty 下的 Eureka Service Registry,另一个用于BarClientJetty 下的端点存根。

Another solution is to manually configure Jetty and Eureka in test application context. I think that this is a better way but in such case you must to understand what @EnableEurekaServerand @EnableDiscoveryClientannotations do with Spring application context.

另一种解决方案是在测试应用程序上下文中手动配置 Jetty 和 Eureka。我认为这是一种更好的方法,但在这种情况下,您必须了解Spring 应用程序上下文@EnableEurekaServer@EnableDiscoveryClient注释的作用。

回答by Alexander

Here is an example how to do the wiring of Feign and WireMock with random port (based on Spring-Boot githubanswer).

这是一个如何使用随机端口连接 Feign 和 WireMock 的示例(基于Spring-Boot github答案)。

@RunWith(SpringRunner.class)
@SpringBootTest(properties = "google.url=http://google.com") // emulate application.properties
@ContextConfiguration(initializers = PortTest.RandomPortInitializer.class)
@EnableFeignClients(clients = PortTest.Google.class)
public class PortTest {

    @ClassRule
    public static WireMockClassRule wireMockRule = new WireMockClassRule(
        wireMockConfig().dynamicPort()
    );

    @FeignClient(name = "google", url = "${google.url}")
    public interface Google {    
        @RequestMapping(method = RequestMethod.GET, value = "/")
        String request();
    }

    @Autowired
    public Google google;

    @Test
    public void testName() throws Exception {
        stubFor(get(urlEqualTo("/"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withBody("Hello")));

        assertEquals("Hello", google.request());
    }


    public static class RandomPortInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {

            // If the next statement is commented out, 
            // Feign will go to google.com instead of localhost
            TestPropertySourceUtils
                .addInlinedPropertiesToEnvironment(applicationContext,
                    "google.url=" + "http://localhost:" + wireMockRule.port()
            );
        }
    }
}

Alternativelyyou can try playing with System.setProperty()in @BeforeClassmethod of your test.

或者你可以尝试播放与System.setProperty()@BeforeClass您的测试方法。

回答by mladzo

Here is an example of using WireMock to test SpringBoot configuration with Feign client and Hystrix fallback.

下面是一个使用 WireMock 测试带有 Feign 客户端和 Hystrix 回退的 SpringBoot 配置的示例。

If you are using Eureka as a server discovery, you need to disable it by setting a property "eureka.client.enabled=false".

如果您使用 Eureka 作为服务器发现,则需要通过设置属性来禁用它"eureka.client.enabled=false"

First, we need to enable the Feign/Hystrix configuration for our application:

首先,我们需要为我们的应用程序启用 Feign/Hystrix 配置:

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@FeignClient(
        name = "bookstore-server",
        fallback = BookClientFallback.class,
        qualifier = "bookClient"
)
public interface BookClient {

    @RequestMapping(method = RequestMethod.GET, path = "/book/{id}")
    Book findById(@PathVariable("id") String id);
}

@Component
public class BookClientFallback implements BookClient {

    @Override
    public Book findById(String id) {
        return Book.builder().id("fallback-id").title("default").isbn("default").build();
    }
}

Please note that we are specifying a fallback class for the Feign client. Fallback class will be called every time Feign client call fails (e.g. connection timeout).

请注意,我们正在为 Feign 客户端指定一个回退类。每次 Feign 客户端调用失败(例如连接超时)时都会调用回退类。

In order for tests to work, we need to configure the Ribbon loadbalancer (will be used internally by Feign client when sending http request):

为了让测试正常工作,我们需要配置 Ribbon 负载均衡器(在发送 http 请求时会被 Feign 客户端内部使用):

@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
        "feign.hystrix.enabled=true"
})
@ContextConfiguration(classes = {BookClientTest.LocalRibbonClientConfiguration.class})
public class BookClientTest {

    @Autowired
    public BookClient bookClient;

    @ClassRule
    public static WireMockClassRule wiremock = new WireMockClassRule(
            wireMockConfig().dynamicPort()));

    @Before
    public void setup() throws IOException {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withHeader("Content-Type", MediaType.APPLICATION_JSON)
                        .withBody(StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("fixtures/book.json"), Charset.defaultCharset()))));
    }

    @Test
    public void testFindById() {
        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("12345"));
    }

    @Test
    public void testFindByIdFallback() {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse().withFixedDelay(60000)));

        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("fallback-id"));
    }

    @TestConfiguration
    public static class LocalRibbonClientConfiguration {
        @Bean
        public ServerList<Server> ribbonServerList() {
            return new StaticServerList<>(new Server("localhost", wiremock.port()));
        }
    }
}

Ribbon server list need to match the url (host and port) of our WireMock configuration.

功能区服务器列表需要匹配我们 WireMock 配置的 url(主机和端口)。

回答by humbaba

There used to be basically two options for doing integration tests for microservices applications:

过去基本上有两种对微服务应用程序进行集成测试的选项:

  1. Deployment of services to a test environment and make end-to-end tests
  2. Mocking other microservices
  1. 将服务部署到测试环境并进行端到端测试
  2. 模拟其他微服务

First option has the obvious disadvantage of the hassle of deploying all the dependencies (other services, databases, etc) as well. In addition, it is slow and hard to debug.

第一个选项的明显缺点是部署所有依赖项(其他服务、数据库等)也很麻烦。此外,它速度慢且难以调试。

Second option is faster and has less hassle but it is easy to end up with stubs that behave differently than the reality in time, due to possible code changes. So it is possible to have successful tests but failing app when deployed to prod.

第二种选择速度更快且麻烦更少,但由于可能的代码更改,很容易最终产生与实际行为不同的存根。因此,在部署到 prod 时,可能会有成功的测试但失败的应用程序。

A better solution would be using consumer driven contract verification, so that you will make sure that provider service's API is compliant with the consumer calls. For this purpose, Spring developers can use Spring Cloud Contract. For other environments, there is a framework called PACT. Both can be used with Feign clients as well. Hereis an example with PACT.

更好的解决方案是使用消费者驱动的合同验证,以便您确保提供者服务的 API 与消费者调用兼容。为此,Spring 开发人员可以使用Spring Cloud Contract。对于其他环境,有一个名为PACT的框架。两者都可以与 Feign 客户端一起使用。是 PACT 的示例。

回答by Siva Tharun

I personally prefer mockServerto stub any restful API, it is easy to use and is similar to wiremock, but is very powerful compared to the latter.

我个人更喜欢mockServer来存根任何restful API,它易于使用并且类似于wiremock,但与后者相比非常强大。

I have attached the sample code written with groovy/spock for stubbing a GET restful call with mockServer.

我附上了用 groovy/spock 编写的示例代码,用于使用 mockServer 存根 GET 宁静调用。

First autowire the mockServer instance in test class

首先在测试类中自动装配 mockServer 实例

@Autowired
private static ClientAndServer mockServer

start the mockServer instance from the setupSpec() method, this method is similar to junit method annotated with @BeforeClass.

从 setupSpec() 方法启动 mockServer 实例,这个方法类似于用@BeforeClass注释的 junit 方法。

def setupSpec() {
     mockServer = ClientAndServer.startClientAndServer(8080)
   }

define the required stub in the corresponding unit test

在相应的单元测试中定义所需的存根

def "test case"() {
 given:
       new MockServerClient("localhost",8080).when(HttpRequest.request().withMethod("GET").withPath("/test/api").withQueryStringParameters(Parameter.param("param1", "param1_value"), Parameter.param("param2", "param2_value"))).respond(HttpResponse.response().withStatusCode(HttpStatus.OK.value()).withBody("{ message: 'sample response' }"))

 when:
 //your code
 then:
 //your code
}

after the execution of test cases, stop the mock server

测试用例执行后,停止模拟服务器

def cleanupSpec() {
     mockServer.stop()
}