繁体   English   中英

如何使用 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 中有一个服务类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 测试带有 Feign 客户端和 Hystrix 回退的 SpringBoot 配置的示例。

如果您使用 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 客户端指定一个回退类。 每次 Feign 客户端调用失败(例如连接超时)时都会调用回退类。

为了让测试工作,我们需要配置 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()));
        }
    }
}

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

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

或者,您可以尝试在测试的@BeforeClass方法中使用System.setProperty()

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

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

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

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

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

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

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

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

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

可能没有办法让 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 应用程序上下文的作用。

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

大多数人已经回答了如何为您的 Feign 客户端进行最少的测试,但让我们将其提升到一个新的水平。

测试普通的 Feign 客户端,请求映射/响应映射/查询映射等只是图片的一小部分。 在微服务环境中,您还必须注意服务弹性,例如客户端负载平衡、断路等。

由于现在是 2021 年,并且 Spring Cloud 将 Hystrix 和 Ribbon 标记为已弃用,因此是时候看看 Resilience4J 了。

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

此外,如果没有进一步解释,这可能有点太理解了,但我无法在单个 stackoverflow 答案中完成,因此您可以查看我的几篇文章以及我的 Feign 课程: 掌握与 Spring Cloud 的微服务通信假装

使用 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 更容易测试。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM