如何使用 spring-cloud-netflix 和 feign 编写集成测试
Posted
技术标签:
【中文标题】如何使用 spring-cloud-netflix 和 feign 编写集成测试【英文标题】:How to write integration tests with spring-cloud-netflix and feign 【发布时间】:2017-01-20 19:35:57 【问题描述】:我使用 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 能够找到它并与之通信?我需要什么样的注释魔法?
【问题讨论】:
我试图为您提供答案,但我知道您的目标很可能不是很好。如果您谈论集成测试,那么您不需要模拟BarClient
逻辑。如果您这样做,那么您的测试将是单元测试,而不是集成。如果它是一个单元测试,那么你可以使用 Mokito 模拟 BarClient
simple,根本不需要 http 请求。我不明白你为什么需要http请求?
我不想构建集成多个微服务的集成测试。当我说集成测试时,我的意思是测试FooService
中所有技术层的集成,而不是只测试一个类并用模拟或存根替换其余部分的单元测试。
你看过RestClientTest,它是Spring Boot 1.4中的MockRestServiceServer
吗?
你找到方法了吗?我正在努力实现同样的目标。在进程外模拟运行所有外部依赖项(例如 Eureka 服务器)的微服务。
正如您在下面的回答中看到的,我切换到了 RestTemplate。
【参考方案1】:
可能没有办法让 WireMock 直接与 Eureka Server 通信,但您可以使用其他变体来配置您需要的测试环境。
-
在测试环境中,您可以在独立的 Jetty servlet 容器下部署 Eureka Service Registry,所有注解都将像在实际生产环境中一样工作。
如果您不想使用真正的
BarClient
端点逻辑,并且集成测试只涉及真正的http
传输层,那么您可以使用Mockito 来进行BarClient
端点存根。
我想为了使用 Spring-Boot 实现 1 和 2,您需要为测试环境创建两个单独的应用程序。一个用于 Jetty 下的 Eureka Service Registry,另一个用于 Jetty 下的 BarClient
端点存根。
另一种解决方案是在测试应用程序上下文中手动配置 Jetty 和 Eureka。我认为这是一种更好的方法,但在这种情况下,您必须了解 @EnableEurekaServer
和 @EnableDiscoveryClient
注释对 Spring 应用程序上下文的作用。
【讨论】:
嗨,谢尔盖,感谢您的回答!在我的服务中添加一个 eureka 注册表听起来不错,但是我怎样才能将我的假BarService
添加到它呢?考虑到您的第二个建议,用 Mockito 替换 BarClient
,是的,我也想这样做,但那是为了单元测试。我还想要一个涉及真正 feign 魔法的集成测试。【参考方案2】:
使用 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,当然,如果你使用RestTemplate它是有效的,但是问题是关于Feign with SpringBoot。人们使用 Feign 是因为它比 RestTemplate 更好...... 这没有回答原始问题。您已经完全改变了您的策略,这意味着原始问题不适用于您,但它仍然是一个有效的问题(这正是我想要做的),这不是它的答案。 @haggisandchips 随便。对我来说它解决了问题:) 真的不明白为什么每个人都那么讨厌我的回答。毕竟,这是我的问题。这是我当时非常适合我的解决方案。无论如何,我不再使用 spring-cloud 了,因为整个事情都是废话和不稳定 也许如果你只是回答了这个问题,那么人们就会投票赞成。顺便说一句,如果您使用发现服务,那么 RestTemplate 是一个非常糟糕的解决方案,因为它们不具备开箱即用的发现意识。【参考方案3】:这里是一个如何使用随机端口进行 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()
。
【讨论】:
您好,谢谢您的回答!这是否涉及尤里卡? google.url 是什么? 您好! 'google.url' 是一个任意属性名称,用于保存被依赖者的服务 url(本例中为 google 主页)。不考虑尤里卡。来到这个解决方案试图覆盖 application.properties 值。 TestPropertySourceUtils 是一个很好的提示。谢谢你。我希望我能以某种方式在官方文档中找到它。【参考方案4】:这是一个使用 WireMock 测试 SpringBoot 配置与 Feign 客户端和 Hystrix 回退的示例。
如果您使用 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 客户端调用失败(例如连接超时)时都会调用 Fallback 类。
为了让测试正常工作,我们需要配置 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(主机和端口)匹配。
【讨论】:
请注意,这种方法依赖于默认禁用的 Hystrix,否则可能会在现场使用回退。 Spring Cloud Dalston 引入了必需的 opt-in 方法,但在此之前 Hystrix 将默认启用。 这应该是公认的答案,但前提是它不测试反序列化。 非常适合我。有几点需要注意:\n如果您使用的是 JUnit 4.11 或更高版本,请添加:@Rule public WireMockClassRule instanceRule = wiremock
另外,如果您正在测试 Spring Boot 2 应用程序,您希望依赖 com.github.tomakehurst:wiremock-jre8:2.21.0
【参考方案5】:
过去基本上有两种方法可以对微服务应用程序进行集成测试:
-
将服务部署到测试环境并进行端到端
测试
模拟其他微服务
第一个选项的明显缺点是部署所有依赖项(其他服务、数据库等)也很麻烦。此外,它的速度慢且难以调试。
第二个选项更快且麻烦更少,但由于可能的代码更改,很容易最终得到与实际情况不同的存根。因此,在部署到 prod 时,可能会有成功的测试但应用程序失败。
更好的解决方案是使用消费者驱动的合同验证,这样您就可以确保提供者服务的 API 与消费者调用兼容。为此,Spring 开发人员可以使用Spring Cloud Contract。对于其他环境,有一个名为PACT 的框架。两者都可以与 Feign 客户端一起使用。 Here 是 PACT 的一个示例。
【讨论】:
【参考方案6】:我个人更喜欢mockServer 来存根任何restful API,它易于使用并且类似于wiremock,但与后者相比非常强大。
我附上了用 groovy/spock 编写的示例代码,用于使用 mockServer 存根 GET restful 调用。
首先在测试类中自动装配 mockServer 实例
@Autowired
private static ClientAndServer mockServer
从 setupSpec() 方法启动 mockServer 实例,该方法类似于 junit 方法用 @BeforeClass 注解。
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()
【讨论】:
【参考方案7】:我认为这是一个非常有趣但被低估的话题,如何在微服务环境中验证您的通信渠道。确保您的渠道按预期工作非常重要,但我仍然看到大量项目花费时间测试他们的 Feign 客户。
大多数人已经回答了如何为您的 Feign 客户做最少的测试,但让我们更上一层楼。
测试普通的 Feign 客户端,请求映射/响应映射/查询映射等只是图片的一小部分。在微服务环境中,您还必须注意服务弹性,例如客户端负载平衡、断路等等。
由于是 2021 年,Spring Cloud 标记了 Hystrix 和 Ribbon 已弃用,是时候看看 Resilience4J。
我不会把代码放在这里,因为它可能读起来可能太多了,但我会给你一些指向我的 GitHub 项目之一的链接。
这是一个带有 Resilience4J 的 Feign 客户端配置,用于断路和使用时间限制器:FeignConfiguration 这是断路器测试:CircuitBreakerTest 这是一个 TimeLimiter 测试:TimeLimiterTest 这是一个客户端负载平衡测试:LoadBalancingTest另外,如果没有进一步的解释,这可能有点太理解了,但我不能在一个 *** 答案中做到这一点,所以你可以查看几个 my articles 以及我关于 Feign 的课程:@987654326 @
【讨论】:
以上是关于如何使用 spring-cloud-netflix 和 feign 编写集成测试的主要内容,如果未能解决你的问题,请参考以下文章
Ribbon之IClientConfigIClientConfigKey
Building Microservices with Spring Cloud - Load balancing
Building Microservices with Spring Cloud - Intelligent Routing