1. 概述
目前,我們的大部分服務都依賴於調用 REST API。Spring 提供了幾種構建 REST 客户端的方法,並且 WebClient 推薦使用。
在本快速教程中,我們將學習如何 對使用 WebClient 調用 API 的服務進行單元測試。
2. 模擬(Mocking)
我們測試中主要有兩種模擬方案:
- 使用 Mockito 模擬 WebClient 的行為
- 使用真實的 WebClient,並通過使用 MockWebServer (okhttp) 模擬其調用的服務
3. 使用 Mockito
Mockito 是 Java 中最常用的 Mock 庫。它擅長提供預定義的響應,但當 Mock 帶有 Fluent API 時,會變得複雜。這是因為 Fluent API 中,大量的對象會傳遞在調用代碼和 Mock 之間。
例如,我們有一個 EmployeeService 類,該類通過使用 WebClient 方法調用 HTTP 檢索數據:
public class EmployeeService {
public EmployeeService(String baseUrl) {
this.webClient = WebClient.create(baseUrl);
}
public Mono<Employee> getEmployeeById(Integer employeeId) {
return webClient
.get()
.uri("http://localhost:8080/employee/{id}", employeeId)
.retrieve()
.bodyToMono(Employee.class);
}
}
我們可以使用Mockito來模擬(mock)這個。
@ExtendWith(MockitoExtension.class)
public class EmployeeServiceUnitTest {
@Test
void givenEmployeeId_whenGetEmployeeById_thenReturnEmployee() {
Integer employeeId = 100;
Employee mockEmployee = new Employee(100, "Adam", "Sandler",
32, Role.LEAD_ENGINEER);
when(webClientMock.get())
.thenReturn(requestHeadersUriSpecMock);
when(requestHeadersUriMock.uri("/employee/{id}", employeeId))
.thenReturn(requestHeadersSpecMock);
when(requestHeadersMock.retrieve())
.thenReturn(responseSpecMock);
when(responseMock.bodyToMono(Employee.class))
.thenReturn(Mono.just(mockEmployee));
Mono<Employee> employeeMono = employeeService.getEmployeeById(employeeId);
StepVerifier.create(employeeMono)
.expectNextMatches(employee -> employee.getRole()
.equals(Role.LEAD_ENGINEER))
.verifyComplete();
}
}正如我們所見,對於鏈中的每一次調用都需要提供不同的 mock 對象,需要四次不同的 when/thenReturn 調用。 這過於冗長且繁瑣。它還要求我們瞭解我們的服務如何精確地使用 WebClient 的實現細節,這使得測試方式變得脆弱。
那麼,我們如何編寫更好的 WebClient 測試呢?
4. 使用 MockWebServer
MockWebServer, 由 Square 團隊構建的小型 Web 服務器,可以接收並響應 HTTP 請求。
與 MockWebServer 進行交互,使我們的代碼能夠使用本地端點的真實 HTTP 調用。 這樣,我們就可以獲得測試預期的 HTTP 交互的益處,而無需應對複雜流式客户端的模擬挑戰。
使用 MockWebServer 建議由 Spring 團隊用於編寫集成測試, 以獲得最佳效果。
4.1. MockWebServer 依賴項
要使用 MockWebServer,我們需要將 okhttp 和 mockwebserver 的 Maven 依賴項添加到我們的 pom.xml 中:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>5.0.0-alpha.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>5.0.0-alpha.12</version>
<scope>test</scope>
</dependency>4.2. 在我們的測試中添加 MockWebServer
讓我們使用 MockWebServer 測試我們的 EmployeeService:
public class EmployeeServiceIntegrationTest {
public static MockWebServer mockBackEnd;
@BeforeAll
static void setUp() throws IOException {
mockBackEnd = new MockWebServer();
mockBackEnd.start();
}
@AfterAll
static void tearDown() throws IOException {
mockBackEnd.shutdown();
}
}在上述 JUnit 測試類中,setUp 和 tearDown 方法負責創建和關閉 MockWebServer。
接下來,需要將實際 REST 服務調用的端口映射到 MockWebServer 的端口:
@BeforeEach
void initialize() {
String baseUrl = String.format("http://localhost:%s",
mockBackEnd.getPort());
employeeService = new EmployeeService(baseUrl);
}
現在是時候創建一個樁(stub),以便 MockWebServer 可以響應一個 HttpRequest。
4.3. 模擬響應
使用 MockWebServer 的便捷 enqueue 方法,可以在 Web 服務器上排隊一個測試響應:
@Test
void getEmployeeById() throws Exception {
Employee mockEmployee = new Employee(100, "Adam", "Sandler",
32, Role.LEAD_ENGINEER);
mockBackEnd.enqueue(new MockResponse()
.setBody(objectMapper.writeValueAsString(mockEmployee))
.addHeader("Content-Type", "application/json"));
Mono<Employee> employeeMono = employeeService.getEmployeeById(100);
StepVerifier.create(employeeMono)
.expectNextMatches(employee -> employee.getRole()
.equals(Role.LEAD_ENGINEER))
.verifyComplete();
}
當實際的 API 調用從 getEmployeeById(Integer employeeId) 方法中執行,MockWebServer 將會返回已排隊的 stub。
4.4. 檢查請求
我們可能也想確保 MockWebServer 接收到了正確的 HttpRequest。
MockWebServer 提供了一個方便的方法 takeRequest,該方法返回一個 RecordedRequest 實例:
RecordedRequest recordedRequest = mockBackEnd.takeRequest();
assertEquals("GET", recordedRequest.getMethod());
assertEquals("/employee/100", recordedRequest.getPath());
使用 RecordedRequest,我們可以驗證收到的 HttpRequest,以確保我們的 WebClient 正確地發送了它
5. 結論
在本文中,我們演示了用於模擬 WebClient 基於 REST 客户端代碼的兩種主要選項。
雖然 Mockito 能夠工作,並且對於簡單的示例來説可能是一個不錯的選擇,但推薦的方法是使用 MockWebServer。