1. 簡介
測試外部依賴項,如 REST API,在 Web 應用程序開發中可能具有挑戰性。網絡調用速度慢且不可靠,因為第三方服務可能不可用或返回意外數據。我們必須找到一種穩健的方法來模擬外部服務,以確保一致且可靠的應用程序測試。這時,WireMock 就派上用場了。
WireMock 是一個強大的 HTTP 模擬服務器,它允許我們對 HTTP 請求進行樁和驗證。 它提供了一個受控的測試環境,確保我們的集成測試快速、可重複且獨立於外部系統。
在本教程中,我們將探索如何將 WireMock 集成到 Spring Boot 項目中,並使用它編寫全面的測試。
2. Maven 依賴
為了使用 WireMock 與 Spring Boot 配合使用,我們需要在我們的 pom.xml 中包含 wiremock-spring-boot 依賴。
<dependency>
<groupId>org.wiremock.integrations</groupId>
<artifactId>wiremock-spring-boot</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>該依賴項提供 WireMock 與 Spring Boot 測試框架之間的無縫集成。
3. 編寫基本 WireMock 測試
在處理更復雜的場景之前,我們首先編寫一個簡單的 WireMock 測試。我們需要確保我們的 Spring Boot 應用程序能夠正確地與外部 API 交互。通過使用 @SpringBootTest 和 @EnableWireMock 註解,WireMock 已在我們的測試環境中啓用。然後,我們可以定義一個簡單的測試用例來驗證 API 的行為:
@SpringBootTest(classes = SimpleWiremockTest.AppConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableWireMock
class SimpleWiremockTest {
@Value("${wiremock.server.baseUrl}")
private String wireMockUrl;
@Autowired
private TestRestTemplate restTemplate;
@Test
void givenWireMockStub_whenGetPing_thenReturnsPong() {
stubFor(get("/ping").willReturn(ok("pong")));
ResponseEntity<String> response = restTemplate.getForEntity(wireMockUrl + "/ping", String.class);
Assertions.assertEquals("pong", response.getBody());
}
@SpringBootApplication
static class AppConfiguration {}
}
在本次測試中,我們使用 註解啓動嵌入式 WireMock 服務器,用於測試環境。 註解從屬性文件中檢索 WireMock 的基本 URL。測試方法 stub 一個 /ping 端點,返回帶有 HTTP 200 狀態碼的 “pong”。然後,我們使用 TestRestTemplate 發送實際的 HTTP 請求,並驗證響應體與預期值匹配。這確保我們的應用程序正確地與模擬的外部服務進行通信。
4. 增加測試複雜度
現在我們已經有一個基本的測試用例,接下來我們將擴展我們的示例,模擬一個返回 JSON 響應並處理各種狀態碼的 REST API。這將幫助我們驗證應用程序如何處理不同的 API 行為。
4.1. 使用 Wiremock 模擬 JSON 響應
在 REST API 中,返回結構化的 JSON 響應是很常見的。我們也可以使用 Wiremock 樁來模擬這種情況:
@Test
void givenWireMockStub_whenGetGreeting_thenReturnsMockedJsonResponse() {
String mockResponse = "{\"message\": \"Hello, Baeldung!\"}";
stubFor(get("/api/greeting")
.willReturn(okJson(mockResponse)));
ResponseEntity<String> response = restTemplate.getForEntity(wireMockUrl + "/api/greeting", String.class);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
Assertions.assertEquals(mockResponse, response.getBody());
}在本測試中,我們模擬一個 GET 請求到 /api/greeting,該請求返回一個包含問候消息的 JSON 響應。然後,我們要求 WireMock 服務器驗證響應狀態碼是否為 200 OK,以及響應體是否與預期的 JSON 結構相匹配。
4.2. 模擬錯誤響應
我們都知道,在網絡開發中,事情並不總是按預期進行,並且某些外部調用可能會返回錯誤。為了做好準備,我們可以模擬錯誤消息,使我們的應用程序能夠以適當的方式響應這些錯誤:
@Test
void givenWireMockStub_whenGetUnknownResource_thenReturnsNotFound() {
stubFor(get("/api/unknown").willReturn(aResponse().withStatus(404)));
ResponseEntity<String> response = restTemplate.getForEntity(wireMockUrl + "/api/unknown", String.class);
Assertions.assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}5. 注入 WireMock 服務器
在更復雜的場景中,我們可能需要管理多個 WireMock 實例或使用特定設置對其進行配置。 WireMock 允許我們使用 標註,注入和配置多個 WireMock 服務器。 這種方法尤其適用於我們的應用程序與大量的外部服務交互,並且我們希望獨立地模擬每個服務。
<h3><strong>5.1. 注入單個 WireMock 服務器</strong></h3>
<p>首先,讓我們將一個 WireMock 服務器注入到我們的測試類中。當需要模擬單個外部服務時,此方法非常有用:</p>
@SpringBootTest(classes = SimpleWiremockTest.AppConfiguration.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableWireMock({
@ConfigureWireMock(name = "user-service", port = 8081),
})
public class InjectedWiremockTest {
@InjectWireMock("user-service")
WireMockServer mockUserService;
@Autowired
private TestRestTemplate restTemplate;
@Test
void givenEmptyUserList_whenFetchingUsers_thenReturnsEmptyList() {
mockUserService.stubFor(get("/users").willReturn(okJson("[]")));
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:8081/users",
String.class);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
Assertions.assertEquals("[]", response.getBody());
}
}與之前的方法不同,通過使用 @EnableWireMock 並在測試類級別啓用 WireMock,無需顯式注入,這種方法允許更精細的控制,通過注入一個命名的 WireMock 服務器實例。 @ConfigureWireMock 註解明確定義了 WireMock 實例的名稱和端口,從而方便在不同的測試用例中管理多個外部服務。
@InjectWireMock(“user-service”) 允許我們直接訪問 WireMockServer 實例,並在測試方法中動態配置和管理其行為。
5.2. 使用多個 WireMock 服務器
在我們的應用程序與多個外部服務交互時,我們可能需要使用多個 WireMock 實例來模擬多個 API。WireMock 允許我們為每個實例配置和指定不同的名稱和端口:
@SpringBootTest(classes = SimpleWiremockTest.AppConfiguration.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@EnableWireMock({
@ConfigureWireMock(name = "user-service", port = 8081),
@ConfigureWireMock(name = "product-service", port = 8082)
})
public class InjectedWiremockTest {
@InjectWireMock("user-service")
WireMockServer mockUserService;
@InjectWireMock("product-service")
WireMockServer mockProductService;
@Autowired
private TestRestTemplate restTemplate;
@Test
void givenUserAndProductLists_whenFetchingUsersAndProducts_thenReturnsMockedData() {
mockUserService.stubFor(get("/users")
.willReturn(okJson("[{\"id\": 1, \"name\": \"John\"}]")));
mockProductService.stubFor(get("/products")
.willReturn(okJson("[{\"id\": 101, \"name\": \"Laptop\"}]")));
ResponseEntity<String> userResponse = restTemplate
.getForEntity("http://localhost:8081/users", String.class);
ResponseEntity<String> productResponse = restTemplate
.getForEntity("http://localhost:8082/products", String.class);
Assertions.assertEquals(HttpStatus.OK, userResponse.getStatusCode());
Assertions.assertEquals("[{\"id\": 1, \"name\": \"John\"}]", userResponse.getBody());
Assertions.assertEquals(HttpStatus.OK, productResponse.getStatusCode());
Assertions.assertEquals("[{\"id\": 101, \"name\": \"Laptop\"}]", productResponse.getBody());
}
}我們隔離了服務,確保對一個模擬服務器的更改不會干擾其他服務器。通過注入多個 WireMock 實例,我們可以完全模擬複雜的服務交互,從而使我們的測試更準確、更可靠。這種方法在微服務架構中尤其有益,因為不同的組件會與各種外部服務進行通信。
6. 結論
WireMock 是一款強大的工具,用於在 Spring Boot 應用程序中測試外部依賴項。 在本文中,我們創建了可靠、可重複且獨立的測試,而無需依賴實際的第三方服務。 我們從一個簡單的測試開始,並將其演變為更高級的場景,包括注入多個 WireMock 服務器。
通過這些技術,我們可以確保我們的應用程序正確處理外部 API 響應,無論它們返回預期數據或錯誤。