測試 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 用戶端傳送要求,並在回應違反契約時擲出錯誤:
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 的應用程式只需要一個測試相依性:
<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 管理其版本。
範例使用以下應用程式邊界:
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 物件。當測試關注分支、回傳值或服務委派時,直接建構控制器可避免啟動內容:
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 排除在測試之外:
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,即可透過完整應用程式內容執行要求,而不需要開啟網路連接埠:
@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 行為屬於測試範圍時,請使用隨機連接埠環境:
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:
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:
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 上閱讀或評論(發表評論需要登入)