知識庫 / Spring / Spring MVC RSS 訂閱

RestTestClient 使用指南

Spring MVC,Spring Web,Testing
HongKong
6
10:35 AM · Dec 06 ,2025

1. 引言

Spring 的測試生態系統已經從基於模擬的 Mock 模擬演變為與嵌入式服務器的完整集成。最新增加入 RestTestClient (Spring Framework 7.0),通過提供簡潔的 Builder 風格的 HTTP 交互界面,而無需傳統客户端的繁瑣,從而彌合了這一差距。這使其成為 MockMvcWebTestClient 的輕量級替代方案,非常適合需要速度、可讀性和靈活性的一體化測試。

在本教程中,我們將 RestTestClient 設置到 Spring Boot 項目中,探索涵蓋基本請求、錯誤處理、斷言等實用示例,並強調最佳實踐,以確保我們的測試具有魯棒性和可維護性。

2. 設置 RestTestClient

要使用 RestTestClient,我們需要一個帶有適當測試依賴的 Spring Boot 項目。

首先,我們將 Spring Boot Test starter 添加到我們的 pom.xml 中:

<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
    }
}

此配置允許我們以任何方式實例化 RestTestClient,在 beforeEach() 方法中。

2.1. 綁定

在編寫測試之前,我們必須決定如何創建我們的 RestTestClient。 其優勢之一在於多種綁定的選項:

  1. 綁定到已初始化好的 MockMvc 實例,用作服務器: bindTo(MockMvc mockMvc)
  2. 綁定到實時服務器: bindToServer(ClientHttpRequestFactory requestFactory)
  3. 綁定到 WebApplicationContextbindToApplicationContext(WebApplicationContext context)
  4. 綁定到 (多個) RouterFunctionbindToRouterFunction(RouterFunction<?>… routerFunctions)
  5. 綁定到 (多個) ControllersbindToController(Object… controllers)

這些選項提供了極高的靈活性,使其成為測試任何 Spring Boot 項目的理想選擇。

2.2. 配置

測試前的最後一步是客户端配置。我們可以通過其構建器對 RestTestClient 進行微調:

restTestClientBuilder
  .baseUrl("/public") // 1
  .defaultHeader("ContentType", "application/json") // 2
  .defaultCookie("JSESSIONID", "abc123def456ghi789") // 3
  .build();

以下三個選項在示例中:

  1. 設置基本 URL,例如前綴 /public
  2. 設置默認標頭,例如內容類型
  3. 設置默認 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. 順利路徑

我們的第一個測試將覆蓋順利路徑,通過向特定ID的個人對象發出GET請求來檢索個人信息:

@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 斷言

<em/>RestTestClient</em/> 與 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. 選擇錯誤的綁定設置

在使用 <em >RestTestClient</em> 時,首先要確定的就是應該使用哪種綁定模式。正如之前提到的,API 支持廣泛的綁定模式。很容易選擇錯誤的模式。

一個常見的陷阱是在我們真正需要完整服務器行為時,使用“mock”綁定(控制器或上下文)。例如,如果我們綁定到控制器,我們可能不會測試完整的 HTTP 棧(servlet 過濾器、Spring Security、全局註冊的消息轉換器)!

錯誤的綁定會導致測試中的假陰性(測試通過,但在生產環境中會失敗)。我們應該確保,綁定的方法和提供的參數對於預期用例有效。

4.2. 線程安全與上下文

<em>RestTestClient</em> 實例在構建完成後即不可變,因此具有線程安全特性,並適合於並行測試執行。這種不可變性確保了沒有共享可變狀態,允許在不產生競爭條件的情況下在測試之間安全地重複使用,這對於加速大型測試套件非常理想。

<em>RestTestClient.Builder</em> 儘管如此,仍然是可變的,並且不可線程安全。在測試或線程之間共享單個構建器實例可能會導致不可預測的配置,例如覆蓋的標頭。

我們應該為每個測試創建一個新的構建器,或者僅使用構建好的不可變實例(推薦)。

4.3. 未注意的行為差異

使用 RestTestClient 存在一個微妙的風險,即與諸如 WebTestClient 這樣其他測試客户端相比。例如,存在一個 公開問題,其中 RestTestClient 在控制器未返回任何主體時,會返回 null 值,而 WebTestClient 則返回一個空 byte[]

這意味着,如果我們編寫斷言以期望一個空主體,並且在客户端和真實服務器之間切換上下文,我們可能會遇到 NPE 或誤導性結果。

最佳實踐:我們應該明確斷言 expectBody().isEmpty() 以確保在沒有主體預期時,而不是依賴 returnResult() 然後檢查 null 與數組。

5. 結論

在本文中,我們探討了 RestTestClient,這是一種現代、簡潔的補充,適用於 Spring Framework 7.0,它簡化了 Spring Boot 中的 REST 集成測試。

該客户端提供靈活的綁定和配置,以及對 JSON、標頭和 Cookie 的表達性斷言,在可讀性和功能性之間取得了平衡,使其成為比其他更重的替代方案更好的選擇。

如往常一樣,代碼可以在 GitHub 上找到

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

發佈 評論

Some HTML is okay.