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

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

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

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

然后我在 Foo 中有一个服务 class SomeService,它调用 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!";
      }

    }
}

现在,为了确保服务之间的通信正常,我想构建一个测试,使用 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!"));
    }
}

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

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

  1. 在测试环境中,您可以在独立的 Jetty servlet 容器下部署 Eureka Service Registry,所有注释将像在真实生产环境中一样工作。
  2. 如果您不想使用真正的 BarClient 端点逻辑,并且集成测试只是关于真正的 http 传输层,那么您可以将 Mockito 用于 BarClient 端点存根.

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

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

使用 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!";
     }
   }
}

使用 Wiremock 比 feign 更容易测试。

这里是一个如何使用随机端口连接 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()
            );
        }
    }
}

Alternatively 你可以尝试在 @BeforeClass 测试方法中使用 System.setProperty()

这里是一个使用 WireMock 测试 SpringBoot 配置的例子,使用 Feign 客户端和 Hystrix fallback。

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

来禁用它

首先,我们需要为我们的应用程序启用 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();
    }
}

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

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

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

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

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

  1. 将服务部署到测试环境并进行端到端 测试
  2. 模拟其他微服务

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

第二个选项更快,麻烦更少,但由于可能的代码更改,很容易最终导致行为与现实不同的存根。因此,有可能测试成功但应用程序部署到产品时失败。

更好的解决方案是使用消费者驱动的合同验证,这样您将确保提供者服务的 API 符合消费者调用。为此,Spring 开发人员可以使用 Spring Cloud Contract. For other environments, there is a framework called PACT. Both can be used with Feign clients as well. Here 是 PACT 的示例。

我个人比较喜欢mockServer to stub any restful API, it is easy to use and is similar to wiremock,但是比起后者就很厉害了

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

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

@Autowired
private static ClientAndServer mockServer

从setupSpec()方法启动mockServer实例,这个方法类似于注解@BeforeClass.

的junit方法
def setupSpec() {
     mockServer = ClientAndServer.startClientAndServer(8080)
   }

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

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
}

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

def cleanupSpec() {
     mockServer.stop()
} 

我认为这是一个非常有趣但被低估的话题,如何在微服务环境中验证您的通信渠道。确保您的频道按预期工作确实很重要,但我仍然看到大量项目花时间测试他们的 Feign 客户端。

大多数人已经回答了如何对 Feign 客户端进行最少的测试,但让我们更进一步。

测试普通的 Feign 客户端,请求 mapping/response mapping/query mapping/etc 只是图片的一小部分。在微服务环境中,您还必须注意服务弹性,例如客户端负载平衡、熔断等等。

既然是 2021 年,Spring Cloud 标记的 Hystrix 和 Ribbon 已弃用,是时候看看 Resilience4J 了。

我不会把代码放在这里,因为它可能读起来太多了,但我会给你一些指向我的 GitHub 项目之一的链接。

此外,如果不做进一步解释,这可能有点过于理解,但我无法在一个 Whosebug 答案中做到这一点,因此您可以查看几个 my articles as well as my course on Feign: Mastering microservice communication with Spring Cloud Feign