測試 Spring Boot REST API

最後更新: 2026-08-17 10:18:23 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

測試 Spring Boot REST API 需要多個範圍,因為直接的 Java 呼叫、模擬的 MVC 要求,以及透過執行中伺服器發出的要求,會分別驗證不同的失敗邊界。有效的測試套件應將大多數行為檢查放在 Spring 內容之外,然後再加入範圍較窄的 Web 切片測試,以及少量的完整應用程式測試。

相依性範例以 Spring Boot 3.x 為目標,因為其測試 starter 使用 JUnit 5。Spring Boot 4 管理 JUnit 6;Jupiter 概念仍然適用,但數個 Boot 測試註解的套件名稱已變更。

不使用測試框架進行測試 #

REST API 不需要 JUnit、Spring Test 或 Mockito 也能進行檢查。啟動應用程式,使用 JDK HTTP 用戶端傳送要求,並在回應違反契約時擲出錯誤:

java
package com.example;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public final class ManualApiCheck {
    public static void main(String[] args) throws Exception {
        var request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080/greetings/Ada"))
                .header("Accept", "application/json")
                .GET()
                .build();

        var response = HttpClient.newHttpClient().send(
                request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new AssertionError("status: " + response.statusCode());
        }
        if (!response.body().equals("{\"message\":\"Hello, Ada\"}")) {
            throw new AssertionError("body: " + response.body());
        }
    }
}

Java 的 assert 陳述式可以取代明確檢查,但除非 JVM 以 -ea 啟動,否則斷言會被停用。因此,明確的 AssertionError 能提供可靠的程序結束碼。

自訂可執行程式仍適合用來對已部署的服務執行冒煙檢查。完全取代測試框架還需要實作測試探索、fixture 生命週期、隔離、結果報告、平行執行和建置整合。手動的 curl 或瀏覽器檢查提供的是探索,而非可重複的回歸測試套件。

選擇測試範圍 #

Spring 的測試文件區分單元測試與整合測試,並提供數個 Web 測試範圍。不存在適用於每個端點的單一推薦註解。請選擇包含待檢查行為的最小範圍:

範圍機制驗證的行為
一般單元測試JUnit 與一般建構函式不使用 Spring 的控制器或服務 Java 邏輯
MVC 切片@WebMvcTest 與 MockMvc路由、繫結、驗證、序列化、篩選器和控制器建議
具有模擬 Web 層的完整內容@SpringBootTest 與 @AutoConfigureMockMvc應用程式組態以及模擬的 Servlet 要求
完整執行中伺服器@SpringBootTest(webEnvironment = RANDOM_PORT)內嵌伺服器、篩選器、序列化、網路和完整應用程式組態
已部署系統測試外部 HTTP 用戶端已封裝的服務及其已部署的相依性

Spring 建議在不需要 Spring 容器時使用單元測試。接著,MockMvc 可在不執行伺服器的情況下提供完整的 Spring MVC 要求處理。仍應保留一些執行中伺服器測試,因為模擬要求無法涵蓋 Servlet 容器的網路、分派和錯誤頁面行為。

使用資料庫或 broker 的測試需要個別的相依性決策:

  • 測試應用程式決策時,將邊界替換為 fake 或 mock。
  • 如果資料庫查詢、對映、交易、broker 行為或線路相容性屬於需求的一部分,請使用隔離的實際服務。
  • 絕對不要讓自動化測試指向生產基礎架構。

Spring Boot 測試相依性 #

已經繼承 Spring Boot 3 parent 或匯入其 BOM 的應用程式只需要一個測試相依性:

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

此 starter 包含 Spring Boot Test、Spring Test、JUnit Jupiter、Mockito、AssertJ、Hamcrest、JSONassert、JsonPath 和 Awaitility。由於這組相依性會作為同一個發行序列進行測試,因此請讓 Spring Boot 管理其版本。

範例使用以下應用程式邊界:

java
public interface GreetingService {
    Greeting greet(String name);
}

public record Greeting(String message) {}

@RestController
@RequestMapping("/greetings")
public final class GreetingController {
    private final GreetingService service;

    public GreetingController(GreetingService service) {
        this.service = service;
    }

    @GetMapping("/{name}")
    Greeting get(@PathVariable String name) {
        return service.greet(name);
    }
}

一般 JUnit 5 測試 #

Spring 元件仍是一般 Java 物件。當測試關注分支、回傳值或服務委派時,直接建構控制器可避免啟動內容:

java
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class GreetingControllerTest {
    @Test
    void returnsTheServiceResult() {
        GreetingService service =
                name -> new Greeting("Hello, " + name);
        var controller = new GreetingController(service);

        assertEquals(
                new Greeting("Hello, Ada"),
                controller.get("Ada"));
    }
}

此測試不會驗證 @GetMapping、路徑變數轉換、JSON 序列化、驗證、篩選器或例外處理器,因為直接的方法呼叫會繞過 Spring MVC。

使用 JUnit 5 的 MVC 切片測試 #

@WebMvcTest 會載入 MVC 基礎架構和選取的 Web 元件,而不是完整應用程式。為控制器相依性提供確定性實作,以便將 Mockito 排除在測試之外:

java
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
@Import(GreetingControllerWebTest.Dependencies.class)
class GreetingControllerWebTest {
    @Autowired
    MockMvc mvc;

    @Test
    void rendersAJsonGreeting() throws Exception {
        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(
                        "application/json"))
                .andExpect(jsonPath("$.message").value("Hello, Ada"));
    }

    @TestConfiguration(proxyBeanMethods = false)
    static class Dependencies {
        @Bean
        GreetingService greetingService() {
            return name -> new Greeting("Hello, " + name);
        }
    }
}

當安全性設定、控制器建議、轉換器或篩選器的行為屬於端點契約時,請將它們加入切片。如果切片排除了正在測試的生產元件,可能在組裝後的應用程式失敗時仍然通過,因此完整內容覆蓋仍然必要。

完整內容測試 #

預設的 @SpringBootTest Web 環境是 MOCK。加入 @AutoConfigureMockMvc,即可透過完整應用程式內容執行要求,而不需要開啟網路連接埠:

java
@SpringBootTest
@AutoConfigureMockMvc
class GreetingApplicationTest {
    @Autowired
    MockMvc mvc;

    @Test
    void servesAGreeting() throws Exception {
        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Hello, Ada"));
    }
}

當內嵌伺服器或實際 HTTP 行為屬於測試範圍時,請使用隨機連接埠環境:

java
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingHttpTest {
    @LocalServerPort
    int port;

    @Test
    void servesAGreetingOverHttp() throws Exception {
        var request = HttpRequest.newBuilder()
                .uri(URI.create(
                        "http://localhost:" + port + "/greetings/Ada"))
                .GET()
                .build();

        var response = HttpClient.newHttpClient().send(
                request, HttpResponse.BodyHandlers.ofString());

        assertEquals(200, response.statusCode());
        assertEquals(
                "{\"message\":\"Hello, Ada\"}",
                response.body());
    }
}

隨機連接埠可防止並行建置期間的固定連接埠衝突。用戶端和伺服器程式碼在不同執行緒上執行,因此在測試執行緒上開始的交易,不會自動回滾由執行中伺服器要求提交的資料。請明確重設狀態,或隔離後端服務。

Boot 3 也提供 TestRestTemplate,而 Spring 的反應式堆疊提供 WebTestClient。Spring Framework 7 新增 RestTestClient,在 MockMvc 和執行中伺服器測試之間提供共用的同步斷言 API。請選擇應用程式所使用 Spring 版本支援的用戶端,而不要直接複製未標示版本的範例。

不使用 Spring 內容的 JUnit 5 與 Mockito #

當 Spring 組態不在測試範圍內時,請使用 MockitoExtension:

java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class GreetingControllerMockitoTest {
    @Mock
    GreetingService service;

    @InjectMocks
    GreetingController controller;

    @Test
    void delegatesToTheService() {
        when(service.greet("Ada"))
                .thenReturn(new Greeting("Welcome, Ada"));

        assertEquals(
                new Greeting("Welcome, Ada"),
                controller.get("Ada"));
        verify(service).greet("Ada");
    }
}

此測試不會載入 Spring 內容。@InjectMocks 會呼叫控制器建構函式,而 @Mock 會建立一般的 Mockito 物件。

Spring 測試內容中的 Mockito #

一般的 @Mock 欄位不會取代 Spring ApplicationContext 中的 Bean。當切片或完整內容測試需要覆寫 Bean 時,請使用 @MockitoBean:

java
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
class GreetingControllerMockitoWebTest {
    @Autowired
    MockMvc mvc;

    @MockitoBean
    GreetingService service;

    @Test
    void rendersTheMockedServiceResult() throws Exception {
        when(service.greet("Ada"))
                .thenReturn(new Greeting("Welcome, Ada"));

        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Welcome, Ada"));

        verify(service).greet("Ada");
    }
}

Spring Framework 的 TestContext 支援會建立 mock,並在重新整理內容之前取代相符的 Bean。Spring Boot 測試註解已經註冊 SpringExtension,因此加入 MockitoExtension 無助於取代內容 Bean。

當需要保留選取的實際行為時,使用 @MockitoSpyBean 包裝現有的內容 Bean。Spring Boot 較舊的 @MockBean 和 @SpyBean 註解在 Boot 3.4 中已被棄用,改為推薦使用 Spring Framework 的 @MockitoBean 和 @MockitoSpyBean。使用 Boot 3.3 或更早版本的專案必須使用舊註解。

請將 Bean 覆寫套用於遠端用戶端、訊息發佈器、時鐘或儲存庫等架構邊界。在完整內容測試中取代大量內部 Bean,會移除選擇 @SpringBootTest 所要驗證的組態;此時,一般單元測試或 MVC 切片能更準確地表達測試範圍。

參考資料 #

評論

在 GitHub 上閱讀或評論(發表評論需要登入)