1. 概述
在本文中,我们将深入探讨如何对 Feign Client 进行集成测试。
首先,我们会创建一个简单的 Open Feign 客户端,并使用 WireMock 编写一个基础的集成测试。
接着,我们会为这个客户端添加 Ribbon 配置,并为其编写对应的集成测试。最后,我们将引入 Eureka 测试容器,验证整个配置是否能正常工作。
2. 创建 Feign Client
为了搭建我们的 Feign Client,首先需要添加 Spring Cloud OpenFeign 的 Maven 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
然后,创建一个简单的 Book
模型类:
public class Book {
private String title;
private String author;
}
最后,定义我们的 Feign Client 接口:
@FeignClient(name = "books-service")
public interface BooksClient {
@RequestMapping("/books")
List<Book> getBooks();
}
✅ 此时我们已经拥有了一个能从 REST 接口获取书籍列表的 Feign Client,接下来我们就可以进行集成测试了。
3. 使用 WireMock 搭建 Mock 服务
3.1. 启动 WireMock 服务
为了测试 BooksClient
,我们需要一个模拟服务来提供 /books
接口。我们的客户端将会向这个模拟服务发起请求。这里我们使用 WireMock 来实现。
首先,添加 WireMock 的 Maven 依赖:
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
<scope>test</scope>
</dependency>
然后配置模拟服务:
@TestConfiguration
@ActiveProfiles("test")
public class WireMockConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public WireMockServer mockBooksService() {
return new WireMockServer(80);
}
@Bean(initMethod = "start", destroyMethod = "stop")
public WireMockServer mockBooksService2() {
return new WireMockServer(81);
}
}
现在我们有两个模拟服务,分别运行在 80 和 81 端口上。
3.2. 配置模拟响应
在 application-test.yml
中添加如下配置,将 books-service
指向 WireMock 服务的端口:
spring:
application:
name: books-service
cloud:
loadbalancer:
ribbon:
enabled: false
discovery:
client:
simple:
instances:
books-service[0]:
uri: http://localhost:80
books-service[1]:
uri: http://localhost:81
接着准备一个模拟响应文件 get-books-response.json
:
[
{
"title": "Dune",
"author": "Frank Herbert"
},
{
"title": "Foundation",
"author": "Isaac Asimov"
}
]
配置模拟接口的响应逻辑:
public class BookMocks {
public static void setupMockBooksResponse(WireMockServer mockService) throws IOException {
mockService.stubFor(WireMock.get(WireMock.urlEqualTo("/books"))
.willReturn(WireMock.aResponse()
.withStatus(HttpStatus.OK.value())
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(
copyToString(
BookMocks.class.getClassLoader().getResourceAsStream("payload/get-books-response.json"),
defaultCharset()))));
}
}
此时,所有配置都已完成,我们可以开始编写第一个测试。
4. 第一个集成测试
创建一个集成测试类 BooksClientIntegrationTest
:
@SpringBootTest
@ActiveProfiles("test")
@EnableFeignClients
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { WireMockConfig.class })
class BooksClientIntegrationTest {
@Autowired
private WireMockServer mockBooksService;
@Autowired
private WireMockServer mockBooksService2;
@Autowired
private BooksClient booksClient;
@BeforeEach
void setUp() throws IOException {
setupMockBooksResponse(mockBooksService);
setupMockBooksResponse(mockBooksService2);
}
//...
}
此时,我们已经配置好了一个 SpringBootTest,当 BooksClient
调用 /books
接口时,会从 WireMock 服务中返回预设的书籍列表。
接下来添加测试方法:
@Test
public void whenGetBooks_thenBooksShouldBeReturned() {
assertFalse(booksClient.getBooks().isEmpty());
}
@Test
public void whenGetBooks_thenTheCorrectBooksShouldBeReturned() {
assertTrue(booksClient.getBooks()
.containsAll(asList(
new Book("Dune", "Frank Herbert"),
new Book("Foundation", "Isaac Asimov"))));
}
✅ 测试通过,说明客户端能正确调用接口并解析响应。
5. 集成 Spring Cloud LoadBalancer
现在我们来为客户端增加 Spring Cloud LoadBalancer 提供的负载均衡能力。
只需要在接口中使用服务名 books-service
替代硬编码 URL:
@FeignClient(name= "books-service")
public interface BooksClient {
...
添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
并在 application-test.yml
中保持相同的配置:
spring:
application:
name: books-service
cloud:
loadbalancer:
ribbon:
enabled: false
discovery:
client:
simple:
instances:
books-service[0]:
uri: http://localhost:80
books-service[1]:
uri: http://localhost:81
再次运行测试,✅ 应该能顺利通过,说明负载均衡配置生效。
5.1. 动态端口配置
如果我们不想硬编码端口,可以配置 WireMock 在启动时动态分配端口。
创建一个新的测试配置类 TestConfig
:
@TestConfiguration
@ActiveProfiles("test")
public class TestConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public WireMockServer mockBooksService() {
return new WireMockServer(options().port(80));
}
@Bean(name="secondMockBooksService", initMethod = "start", destroyMethod = "stop")
public WireMockServer secondBooksMockService() {
return new WireMockServer(options().port(81));
}
}
该配置会动态启动两个服务实例,用于负载均衡测试。
5.2. 负载均衡测试
配置好负载均衡后,我们来验证客户端是否能在两个服务实例之间正确切换:
@SpringBootTest
@ActiveProfiles("test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { TestConfig.class })
class LoadBalancerBooksClientIntegrationTest {
@Autowired
private WireMockServer mockBooksService;
@Autowired
private WireMockServer secondMockBooksService;
@Autowired
private BooksClient booksClient;
@Autowired
private LoadBalancerClientFactory clientFactory;
@BeforeEach
void setUp() throws IOException {
setupMockBooksResponse(mockBooksService);
setupMockBooksResponse(secondMockBooksService);
String serviceId = "books-service";
RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer(ServiceInstanceListSuppliers
.toProvider(serviceId, instance(serviceId, "localhost", false), instance(serviceId, "localhost", true)),
serviceId, -1);
}
private static DefaultServiceInstance instance(String serviceId, String host, boolean secure) {
return new DefaultServiceInstance(serviceId, serviceId, host, 80, secure);
}
@Test
void whenGetBooks_thenRequestsAreLoadBalanced() {
for (int k = 0; k < 10; k++) {
booksClient.getBooks();
}
mockBooksService.verify(
moreThan(0), getRequestedFor(WireMock.urlEqualTo("/books")));
secondMockBooksService.verify(
moreThan(0), getRequestedFor(WireMock.urlEqualTo("/books")));
}
@Test
public void whenGetBooks_thenTheCorrectBooksShouldBeReturned() {
assertTrue(booksClient.getBooks()
.containsAll(asList(
new Book("Dune", "Frank Herbert"),
new Book("Foundation", "Isaac Asimov"))));
}
}
✅ 测试通过表示负载均衡机制运行正常。
6. Eureka 集成测试
前面我们测试了基于 LoadBalancer 的客户端,但如果我们的系统使用的是 Eureka 服务发现机制,该如何测试呢?
为此,我们将使用 Testcontainers 启动一个 Eureka 服务,并注册一个模拟的 books-service
,然后进行集成测试。
首先添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
6.1. TestContainer 配置
创建一个 Eureka 服务的 TestContainer 配置:
public class EurekaContainerConfig {
public static class Initializer implements ApplicationContextInitializer {
public static GenericContainer eurekaServer =
new GenericContainer("springcloud/eureka").withExposedPorts(8761);
@Override
public void initialize(@NotNull ConfigurableApplicationContext configurableApplicationContext) {
Startables.deepStart(Stream.of(eurekaServer)).join();
TestPropertyValues
.of("eureka.client.serviceUrl.defaultZone=http://localhost:"
+ eurekaServer.getFirstMappedPort().toString()
+ "/eureka")
.applyTo(configurableApplicationContext);
}
}
}
6.2. 注册模拟服务
创建一个模拟服务控制器:
@Configuration
@RestController
@ActiveProfiles("eureka-test")
public class MockBookServiceConfig {
@RequestMapping("/books")
public List getBooks() {
return Collections.singletonList(new Book("Hitchhiker's Guide to the Galaxy", "Douglas Adams"));
}
}
在 application-eureka-test.yml
中配置服务名:
spring:
application:
name: books-service
⚠️ 注意:由于 netflix-eureka-client
已加入依赖,默认会启用 Eureka。如果不想启用 Eureka,可在非 Eureka 测试中设置 eureka.client.enabled=false
。
6.3. 集成测试
整合所有配置,编写测试类:
@ActiveProfiles("eureka-test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = { MockBookServiceConfig.class },
initializers = { EurekaContainerConfig.Initializer.class })
class ServiceDiscoveryBooksClientIntegrationTest {
@Autowired
private BooksClient booksClient;
@Lazy
@Autowired
private EurekaClient eurekaClient;
@BeforeEach
void setUp() {
await().atMost(60, SECONDS).until(() -> eurekaClient.getApplications().size() > 0);
}
@Test
public void whenGetBooks_thenTheCorrectBooksAreReturned() {
List books = booksClient.getBooks();
assertEquals(1, books.size());
assertEquals(
new Book("Hitchhiker's guide to the galaxy", "Douglas Adams"),
books.stream().findFirst().get());
}
}
✅ 测试通过,说明 Eureka 服务发现机制运行正常。
7. 总结
在本文中,我们探讨了如何为 Spring Cloud Feign Client 编写集成测试:
- 使用 WireMock 进行基础测试
- 添加 Ribbon 实现负载均衡测试
- 引入 Eureka 实现服务发现测试
每一步都通过实际测试验证了客户端在不同环境下的行为。
完整代码可参考:GitHub 项目地址