如何使用 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 通信,但您可以使用其他变体来配置您需要的测试环境。
- 在测试环境中,您可以在独立的 Jetty servlet 容器下部署 Eureka Service Registry,所有注释将像在真实生产环境中一样工作。
- 如果您不想使用真正的
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(主机和端口)相匹配。
过去基本上有两种选择来对微服务应用程序进行集成测试:
- 将服务部署到测试环境并进行端到端
测试
- 模拟其他微服务
第一个选项的明显缺点是部署所有依赖项(其他服务、数据库等)也很麻烦。此外,它很慢且难以调试。
第二个选项更快,麻烦更少,但由于可能的代码更改,很容易最终导致行为与现实不同的存根。因此,有可能测试成功但应用程序部署到产品时失败。
更好的解决方案是使用消费者驱动的合同验证,这样您将确保提供者服务的 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 项目之一的链接。
- 这是一个带有 Resilience4J 的 Feign 客户端配置,用于断路和使用时间限制器:FeignConfiguration
- 这是一个断路器测试:CircuitBreakerTest
- 这是一个 TimeLimiter 测试:TimeLimiterTest
- 这是一个客户端负载均衡测试:LoadBalancingTest
此外,如果不做进一步解释,这可能有点过于理解,但我无法在一个 Whosebug 答案中做到这一点,因此您可以查看几个 my articles as well as my course on Feign: Mastering microservice communication with Spring Cloud 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 通信,但您可以使用其他变体来配置您需要的测试环境。
- 在测试环境中,您可以在独立的 Jetty servlet 容器下部署 Eureka Service Registry,所有注释将像在真实生产环境中一样工作。
- 如果您不想使用真正的
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(主机和端口)相匹配。
过去基本上有两种选择来对微服务应用程序进行集成测试:
- 将服务部署到测试环境并进行端到端 测试
- 模拟其他微服务
第一个选项的明显缺点是部署所有依赖项(其他服务、数据库等)也很麻烦。此外,它很慢且难以调试。
第二个选项更快,麻烦更少,但由于可能的代码更改,很容易最终导致行为与现实不同的存根。因此,有可能测试成功但应用程序部署到产品时失败。
更好的解决方案是使用消费者驱动的合同验证,这样您将确保提供者服务的 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 项目之一的链接。
- 这是一个带有 Resilience4J 的 Feign 客户端配置,用于断路和使用时间限制器:FeignConfiguration
- 这是一个断路器测试:CircuitBreakerTest
- 这是一个 TimeLimiter 测试:TimeLimiterTest
- 这是一个客户端负载均衡测试:LoadBalancingTest
此外,如果不做进一步解释,这可能有点过于理解,但我无法在一个 Whosebug 答案中做到这一点,因此您可以查看几个 my articles as well as my course on Feign: Mastering microservice communication with Spring Cloud Feign