Spring Cloud Feign 集成测试

1、概览

本文将带你了解 Feign 客户端的集成测试。

首先创建一个基本的 Open Feign 客户端,并使用 WireMock 编写一个简单的集成测试。

之后,给客户端添加 Ribbon 配置,并为其构建一个集成测试。最后,配置一个 Eureka 测试容器,并测试此设置,以确保整个配置按预期工作。

2、Feign Client

要设置 Feign 客户端,首先要添加 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 客户端接口:

@FeignClient(name = "books-service")
public interface BooksClient {

    @RequestMapping("/books")
    List<Book> getBooks();
}

现在,我们有了一个从 REST 服务中获取 List<Book> 的 Feign 客户端。接下来,编写一些集成测试。

3、WireMock

3.1、设置 WireMock 服务器

要测试 BooksClient,需要一个提供 /books 端点的 mock 服务,客户端将调用该 mock 服务。为此,我们使用 WireMock。

添加 WireMock Maven 依赖:

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
</dependency>

配置 mock server:

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

现在,我们有两个正在运行的 Mock 服务器,监听 8081 端口的连接。

3.2、设置 Mock

application-test.yml 中添加指向 WireMockServer 端口的 book-service.url 属性:

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

也为 /books 端点准备一个 mock 响应:get-books-response.json

[
  {
    "title": "Dune",
    "author": "Frank Herbert"
  },
  {
    "title": "Foundation",
    "author": "Isaac Asimov"
  }
]

现在,为 /books 端点上的 GET 请求配置 mock 响应:

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 配置了一个 WireMockServer,当 BooksClient 调用 /books 端点时,它将返回预定义的 Books 列表。

最后,添加测试方法:

@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 提供的负载均衡功能来改进客户端。

在客户端接口中,删除硬编码的 service URL,改用服务名称 book-service 来引用该服务:

@FeignClient(name= "books-service")
public interface BooksClient {
...

接下来,添加 maven 依赖:

<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

再次运行 BooksClientIntegrationTest。测试通过,新设置按预期运行。

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));
    }
}

该配置设置了两个 WireMock 服务器,每个服务器都运行在运行时动态分配的不同端口上。此外,还用这两个 Mock 服务器配置了 Ribbon Server 列表。

5.2、负载均衡测试

配置好 Ribbon Load Balancer 后进行测试,确保 BooksClient 在两个 Mock 服务器之间正确进行负载均衡请求:

@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

到目前为止,我们已经了解了如何测试使用 Spring Cloud LoadBalancer 进行负载均衡的客户端。但如果我们的设置使用了像 Eureka 这样的服务发现系统呢?

应该编写一个集成测试,确保 BooksClient 在这种情况下也能按预期运行。

为此,运行一个 Eureka 服务器作为测试容器。然后,在 Eureka 容器中启动并注册一个 Mock book-service。安装就绪后,就可以对其运行测试了。

添加 TestcontainersNetflix Eureka Client Maven 依赖:

<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 设置

创建一个 TestContainer 配置,它将启动 Eureka 服务器:

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);
        }
    }
}

如上,上面的 initializer 启动了容器。然后,暴露了 Eureka 服务器正在监听的 8761 端口。

最后,在 Eureka 服务启动后,需要更新 eureka.client.serviceUrl.defaultZone 属性。这定义了用于服务发现的 Eureka 服务器地址。

6.2、注册 Mock Server

Eureka 服务器已经启动并运行,现在需要注册一个 Mock books-service

创建一个 RestController

@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"));
    }
}

要注册这个 Controller,需要确保 application-eureka-test.yml 中的 spring.application.name 属性为 books-service,与 BooksClient 接口中使用的服务名称(service name)相同。

注意:既然 netflix-eureka-client 库已在依赖列表中,那么 Eureka 将默认用于服务发现。因此,如果希望之前不使用 Eureka 的测试仍然能通过,就需要手动将 eureka.client.enabled 设置为 false。这样,即使 Eureka 库在 classpath 上,BooksClient 也不会尝试使用 Eureka 查找服务,而是使用 Ribbon 配置。

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

}

首先,EurekaContainerConfig 中的 Context Initializer 会启动 Eureka 服务。

然后,SpringBootTest 启动 books-service 应用,该应用暴露 MockBookServiceConfig 中定义的 Controller。

由于 Eureka 容器和 Web 应用的启动可能需要几秒钟,因此需要等待 books-service 注册完成。

最后,测试方法验证 BooksClientEureka 配置的整合是否成功。

7、总结

本文介绍了为 Spring Cloud Feign 客户端编写集成测试的不同方法。首先使用 WireMock 对基本的客户端进行测试。然后测试与 Ribbon 负载均衡的整合,最后测试与 Eureka 服务发现功能的整合。


Ref:https://www.baeldung.com/spring-cloud-feign-integration-tests