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 项目地址


原始标题:Integration Tests With Spring Cloud Netflix and Feign