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 服務發現。再次確認客户端仍然按照預期工作。