知識庫 / Testing RSS 訂閱

Spring Cloud Netflix 和 Feign 集成測試

Spring Cloud,Testing
HongKong
5
12:43 PM · Dec 06 ,2025

1. 概述

本文將探討 Feign Client的集成測試

我們將創建一個基本的 Open Feign Client,併為此編寫一個簡單的集成測試,藉助 WireMock 實現。

之後,我們將向該客户端添加 Ribbon 配置,併為之構建集成測試。 最終,我們將配置 Eureka 測試容器並測試該配置,以確保整個配置按預期工作。

2. Feign 客户端

為了設置我們的 Feign 客户端,我們首先應該添加 Spring Cloud OpenFeign Maven 依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<p>之後,讓我們為我們的模型創建一個 <em >Book</em> 類:</p>
public class Book {
    private String title;
    private String author;
}

最後,讓我們創建我們的 Feign 客户端接口:

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

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

現在,我們有一個 Feign 客户端,它從一個 REST 服務中檢索一列表書籍。現在,讓我們繼續前進,編寫一些集成測試。

3. WireMock

WireMock 是一個用於創建 Mock 服務的庫。它允許您在測試中模擬外部依賴項,例如 Web 服務、數據庫或消息隊列。WireMock 提供了多種方式來創建 Mock 響應,包括:

  • HTTP 響應: WireMock 可以模擬各種 HTTP 狀態碼、頭部和響應體。
  • JSON 響應: 您可以輕鬆地創建 JSON 格式的 Mock 響應。
  • XML 響應: 同樣,WireMock 也支持模擬 XML 格式的響應。
  • 自定義響應: 您可以編寫自定義的響應邏輯,以滿足您的特定需求。

WireMock 提供了豐富的 API,方便您在測試代碼中集成 Mock 服務。它還支持各種測試框架,例如 JUnit、Mockito 和 Spock。

以下是一個使用 WireMock 創建 Mock 響應的示例:

// Java 代碼示例
import io.wiremock.MockResponse;
import io.wiremock.Response;

Response response = new MockResponse()
        .setStatus(200)
        .addHeader("Content-Type", "application/json")
        .addBody("{\"name\": \"John Doe\", \"age\": 30}")
        .build();

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. 搭建 Mock 環境

讓我們將 book-service url 屬性添加到 application-test.yml 文件中,指向 WireMockServer 的端口:

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 用於 /books 端點:

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

現在我們來配置對 GET請求在 /books端點的模擬響應:

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 提供的負載均衡功能來改進客户端。

在客户端接口中,我們只需要移除硬編碼的服務 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 服務器,每個服務器在運行時動態分配不同的端口運行。此外,它還配置了 Ribbon 服務器列表,使其包含這兩個 Mock 服務器。

5.2. 負載均衡測試

現在我們已經配置好 Ribbon 負載均衡器,讓我們確保 BooksClient 正確地在兩個模擬服務器之間切換:

@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 之類的服務發現系統,該怎麼辦?我們應該編寫一個集成測試,以確保我們的 <em >BooksClient</em> 在這種情況下也能正常工作。

為此,我們將 Eureka 服務器作為測試容器運行。然後,我們啓動並註冊一個模擬 <em >book-service</em> 到我們的 Eureka 容器中。最後,在安裝完成後,我們可以運行測試。

在繼續之前,讓我們添加 <a href="https://mvnrepository.com/artifact/org.testcontainers/testcontainers">Testcontainers</a><a href="https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-eureka-client">Netflix Eureka Client</a> 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);
        }
    }
}

如我們所見,上述的初始化器啓動了容器。然後它暴露了端口 8761, Eureka 服務器在此監聽。

最後,在 Eureka 服務啓動後,我們需要更新 eureka.client.serviceUrl.defaultZone 屬性。該屬性定義了用於服務發現的 Eureka 服務器地址。

6.2. 模擬服務註冊

現在我們的 Eureka 服務器已經運行起來,我們需要註冊一個模擬的 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"));
    }
}

為了註冊這個控制器,我們現在只需要確保 spring.application.name 屬性在我們的 application-eureka-test.yml 文件中設置為 books-service,與 BooksClient 接口中使用的服務名稱相同。

注意:現在 netflix-eureka-client 庫已添加到我們的依賴項中,Eureka 將默認用於服務發現。因此,如果我們的舊測試用例未使用 Eureka,為了使它們繼續通過,我們需要手動將 eureka.client.enabled 設置為false。 這樣,即使庫位於路徑上,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 內部的上下文初始化器啓動 Eureka 服務。

然後,SpringBootTest 啓動了暴露在 books-service 應用程序中的控制器,該控制器定義在 MockBookServiceConfig 中。

因為 Eureka 容器和 Web 應用程序的啓動可能需要幾秒鐘,所以我們需要等待 books-service 註冊成功。這發生在測試的 setUp 方法中。

最後,測試方法驗證 BooksClient 與 Eureka 配置的組合是否正確工作。

7. 結論

在本文中,我們探討了如何為 Spring Cloud Feign 客户端編寫集成測試的不同方法。我們首先使用 WireMock 測試了一個基本客户端。隨後,我們添加了 Ribbon 的負載均衡功能。我們編寫了一個集成測試,並確保 Feign 客户端能夠正確地與 Ribbon 提供的客户端負載均衡功能協同工作。最後,我們加入了 Eureka 服務發現。再次確認客户端仍然按照預期工作。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.