RestTestClient 指南
1. 引言
Spring 的測試生態系統已經從基於模擬的測試發展到與嵌入式伺服器的完全整合。 Spring Framework 7.0 中的最新成員RestTestClient彌合了這一差距,它提供了一個簡潔的、構建器風格的 HTTP 交互接口,無需傳統客戶端的繁瑣操作。這使其成為MockMvc或WebTestClient –非常適合需要速度、可讀性和靈活性的整合測試。
在本教程中,我們將在 Spring Boot 專案中設定RestTestClient ,探索涵蓋基本請求、錯誤處理、斷言等的實際範例,並重點介紹最佳實踐,以確保我們的測試健壯且易於維護。
2. 設定RestTestClient
要使用RestTestClient ,我們需要一個具有對應測試依賴項的 Spring Boot 專案。
首先,讓我們在pom.xml中包含Spring Boot Test starter :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
我們還必須確保使用Spring Framework 7.0或更高版本,因為RestTestClient是較新的元件。
接下來,我們配置一個帶有@SpringBootTest註解的測試類別來載入應用程式上下文:
@SpringBootTest
class RestTestClientUnitTest {
private RestTestClient restTestClient;
@BeforeEach
void beforeEach() {
// TODO: initialize restTestClient
}
}
這種設定允許我們在beforeEach()方法中以任何我們喜歡的方式實例化RestTestClient 。
2.1. 結合
在編寫測試之前,我們必須決定如何建立RestTestClient 。它的優勢之一是提供了多種綁定選項:
- 綁定到已初始化的
MockMvc實例以用作伺服器:bindTo(MockMvc mockMvc) - 綁定到即時伺服器:
bindToServer(ClientHttpRequestFactory requestFactory) - 綁定到
WebApplicationContext:bindToApplicationContext(WebApplicationContext context) - 綁定到(多)
RouterFunction:bindToRouterFunction(RouterFunction<?>… routerFunctions) - 綁定到(多個)
Controllers:bindToController(Object… controllers)
這些選項帶來了高度的靈活性,使其成為測試任何 Spring Boot 專案的最佳選擇。
2.2 配置
測試前的最後一步是客戶端配置。我們可以透過建構器對RestTestClient進行微調:
restTestClientBuilder
.baseUrl("/public") // 1
.defaultHeader("ContentType", "application/json") // 2
.defaultCookie("JSESSIONID", "abc123def456ghi789") // 3
.build();
範例中的三個選項是:
- 設定基本 URL,例如前綴
/public - 設定預設標頭,例如內容類型
- 設定預設 cookie,例如會話 ID
設定完畢,我們準備進行第一次測試。
3. 實際案例
讓我們探討幾個場景來突出RestTestClient'靈活性,包括不同類型的用例和複雜的斷言。
以下測試將綁定到我們的控制器並針對該控制器執行:
@RestController("my")
class MyController {
@GetMapping("/persons/{id}")
public ResponseEntity<Person> getPersonById(@PathVariable Long id) {
return id == 1
? ResponseEntity.ok(new Person(1L, "John Doe"))
: ResponseEntity.noContent().build();
}
}
getPersonById()方法傳回的是Person實體:
record Person(Long id, String name) { }
3.1. 快樂路徑
我們的第一個測試將涵蓋正常流程,即透過呼叫 GET 請求,根據 id 獲取人員資訊:
@Test
void givenValidPath_whenCalled_thenReturnOk() {
restTestClient.get() // 1
.uri("/persons/1") // 2
.accept(MediaType.APPLICATION_JSON) // 3
.exchange() // 4
.expectStatus() // 5
.isOk() // 6
.expectBody(Person.class) // 7
.isEqualTo(new Person(1L, "John Doe")); // 8
}
我們使用RestTestClient的 API 來 (1) 初始化一個指向 (2) 特定路徑的請求,並 (3) 新增適當的 Accept 請求頭。然後,我們 (4) 執行該請求,並 (5) 檢查其狀態是否符合預期-在本例中為 OK (6)。最後,我們 (7) 將請求體轉換為Person類型,並 (8) 將其斷言為預期實例。
這看起來很簡單,但是我們要如何檢查請求是否失敗?
3.2. 簡單錯誤狀況
我們的第二個測試涵蓋客戶端錯誤(錯誤的 HTTP 方法):
@Test
void givenWrongMethod_whenCalled_thenReturnClientError() {
restTestClient.post() // <== wrong method
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.is4xxClientError();
}
以下是我們檢查NO_CONTENT響應(無效 ID)的方法:
@Test
void givenWrongId_whenCalled_thenReturnNoContent() {
restTestClient.get()
.uri("/persons/0") // <== wrong id
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isNoContent();
}
3.3. JSON 斷言
RestTestClient與 JSON Path 無縫集成,可實現詳細的請求體斷言:
@Test
void givenValidId_whenGetPerson_thenReturnsCorrectFields() {
restTestClient.get()
.uri("/persons/1")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("$.id").isEqualTo(1)
.jsonPath("$.name").isEqualTo("John Doe");
}
這樣既避免了對物件進行完全反序列化,又能精確地驗證結構和值。
3.4 自訂斷言
如果我們希望針對更複雜的場景使用自訂斷言,我們可以使用consumeWith()方法:
@Test
void givenValidRequest_whenGetPerson_thenPassesAllAssertions() {
restTestClient.get()
.uri("/persons/1")
.exchange()
.expectStatus()
.isOk()
.expectBody(Person.class)
.consumeWith(result -> {
assertThat(result.getStatus().value()).isEqualTo(200);
assertThat(result.getResponseBody().name()).isNotNull().isEqualTo("John Doe");
});
}
在我們提供的Consumer<EntityExchangeResult<B>>中,我們可以使用任何我們想要的斷言,例如 AssertJ 函式庫。
3.5. 多控制器
最後,我們將探討同時使用多個控制器的情況。
RestTestClient允許綁定多個控制器:
restTestClient = RestTestClient.bindToController(myController, anotherController)
.build();
現在我們可以在同一個測試類別中寫一個測試,斷言第二個控制器的端點:
@Test
void givenValidQueryToSecondController_whenGetPenguinMono_thenReturnsEmpty() {
restTestClient.get()
.uri("/pink/penguin")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk()
.expectBody(Penguin.class)
.value(it -> assertThat(it).isNull());
}
這種方法適用於測試複合 API 或模組化服務,確保控制器之間的互動能夠如預期運作,而無需完全啟動伺服器。
4. 最佳實務與迷思
既然我們已經了解了基礎知識,接下來讓我們深入探討新的RestTestClient.
4.1 選擇錯誤的綁定設定
使用RestTestClient時,首先要正確操作的事項之一是**選擇合適的綁定模式**。如前所述,該 API 支援多種綁定模式,很容易選錯。
一個常見的陷阱是,當我們真正需要完整的伺服器行為時,卻使用了「模擬」綁定(控制器或上下文)。例如,如果我們綁定到控制器,就可能無法執行完整的 HTTP 協定堆疊(servlet 過濾器、Spring Security、全域註冊的訊息轉換器)!
錯誤的綁定會導致測試出現假陰性結果(測試通過,但在生產環境中會失敗)。我們應該確保綁定方法和提供的參數對於預期的使用情境是有效的。
4.2 線程安全性和上下文
RestTestClient實例一旦建置完成便不可變,因此具有執行緒安全性,適合併行測試執行。這種不可變性確保了不存在共享的可變狀態,從而允許在不同測試之間安全地重複使用,避免競態條件,這對於加速大型測試套件的運作至關重要。
然而, RestTestClient.Builder是可變的,並且不是線程安全的。在多個測試或執行緒之間共用同一個建構器實例可能會導致不可預測的配置,例如覆蓋標頭。
我們應該為每個測試建立一個新的建構器,或只使用已建置的不可變實例(建議)。
4.3. 未被察覺的行為差異
RestTestClient的一個棘手風險在於它與其他測試客戶端(例如WebTestClient )相比存在一些細微的行為差異。例如,目前存在一個未解決的問題:當控制器沒有回傳響應體時, RestTestClient呼叫returnResult().getResponseBody()會傳回null ,而WebTestClient則會傳回一個空的byte[] 。
這意味著,如果我們編寫斷言時期望得到一個空主體,並且我們切換了上下文(客戶端與真實伺服器),我們可能會遇到空指標異常或誤導性的結果。
最佳實踐:**當沒有預期的內容時,我們應該明確地斷言expectBody().isEmpty()**而不是依賴returnResult()然後檢查null與數組。
5. 結論
在本文中,我們探討了RestTestClient ,它是 Spring Framework 7.0 的一個現代化、流暢的新增功能,可以簡化 Spring Boot 中的 REST 整合測試。
從靈活的綁定和配置到對 JSON、標頭和 cookie 的富有表現力的斷言,它在可讀性和功能性之間取得了平衡——使其成為比更笨重的替代方案的絕佳選擇。
和往常一樣,程式碼可以在 GitHub 上找到。