Spring Boot測試終極指南:從環境搭建到分層測試實戰
掌握MockMvc與分層測試策略,讓你的代碼質量提升一個維度
一、環境搭建:Maven依賴深度解析
Spring Boot測試的核心依賴在pom.xml中配置如下:
<dependencies><!-- 核心測試庫:包含JUnit 5、Mockito、AssertJ等 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- MockMvc獨立測試支持 --><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><scope>test</scope></dependency><!-- AssertJ流式斷言庫 --><dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.24.2</version><scope>test</scope></dependency>
</dependencies>
關鍵依賴詳解:
- spring-boot-starter-test:測試核心包,包含:
- JUnit Jupiter(JUnit 5測試引擎)
- Mockito(模擬依賴對象)
- JSONPath(JSON數據解析)
- AssertJ(流式斷言)
- spring-test:提供MockMvc等Spring測試工具類
- assertj-core:增強版斷言庫,支持鏈式調用
為什么選擇JUnit 5而不是JUnit 4?
JUnit 5的模塊化架構(Jupiter+Platform+Vintage)支持Lambda表達式、參數化測試等新特性,且與Spring Boot 2.2+深度集成:// JUnit 5示例(支持Lambda) @Test @DisplayName("測試商品價格計算") void testPriceCalculation() {assertAll(() -> assertThat(calculator.calculate(10)).isEqualTo(100),() -> assertThatThrownBy(() -> calculator.calculate(-1)).isInstanceOf(IllegalArgumentException.class)); }
二、核心概念深度剖析
1. 應用上下文(Application Context)
Spring容器核心,管理Bean的生命周期和依賴注入:
@SpringBootTest
public class ContextLoadTest {@Autowiredprivate ApplicationContext ctx; // 注入應用上下文@Testvoid testBeanExistence() {assertThat(ctx.containsBean("productService")).isTrue();}
}
通過@SpringBootTest
加載完整上下文,適合集成測試
2. 斷言(Assertions)
驗證代碼行為的檢查點,分為:
- 基礎斷言:驗證true/false、相等性等
// JUnit基礎斷言 assertEquals(expected, actual);
- 流式斷言(AssertJ):提升可讀性
// AssertJ鏈式斷言 assertThat(product).hasFieldOrProperty("name").hasFieldOrPropertyWithValue("price", 99.9).extracting(Product::getCategory).isEqualTo("電子產品");
AssertJ的錯誤信息更直觀,支持集合、異常等復雜驗證
三、MockMvc全解:Controller層隔離測試
1. 核心組件
- MockMvc:模擬HTTP請求的工具
- standaloneSetup:獨立構建控制器,不加載完整上下文
適用于純Controller邏輯測試,避免加載無關BeanmockMvc = MockMvcBuilders.standaloneSetup(productController).build();
2. HTTP請求模擬鏈
// 測試商品查詢接口
mockMvc.perform(get("/api/products") // ① 模擬GET請求.param("category", "電子") // 添加查詢參數.contentType(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) // ② 驗證HTTP狀態碼.andExpect(jsonPath("$[0].id").value(1001)) // ③ JSONPath驗證.andDo(print()); // ④ 打印請求詳情
- perform():發起HTTP請求(支持GET/POST/PUT/DELETE)
- andExpect():驗證響應結果(狀態碼、Header、Body)
- jsonPath():使用JSONPath語法定位JSON字段
// 驗證返回的JSON數組中第一個元素的name字段 .andExpect(jsonPath("$[0].name").value("華為手機"))
- andDo():執行附加操作(如打印日志、保存結果)
3. 請求體處理
// 測試新增商品
ProductQuery query = new ProductQuery("手機", 1000, 2000); // ① 構建查詢對象
String json = objectMapper.writeValueAsString(query); // ② 轉為JSONmockMvc.perform(post("/api/products").content(json).contentType(MediaType.APPLICATION_JSON)).andExpect(status().isCreated());
ProductQuery
是參數封裝對象,用于接收查詢條件
四、分層測試策略:Controller vs Service
1. 為什么分層測試?
測試類型 | 測試目標 | 使用注解 | 特點 |
---|---|---|---|
Controller | HTTP接口邏輯 | @WebMvcTest | 模擬HTTP請求,不啟動完整服務 |
Service | 業務邏輯正確性 | @SpringBootTest | 加載完整上下文,測試真實邏輯 |
Repository | 數據訪問層 | @DataJpaTest | 使用內存數據庫 |
分層測試實現關注點分離,避免測試復雜度爆炸
2. Controller層測試示例
@WebMvcTest(ProductController.class) // 只加載Controller相關Bean
public class ProductControllerTest {@Autowiredprivate MockMvc mockMvc;@MockBeanprivate ProductService productService; // 模擬Service@Testvoid testGetProduct() throws Exception {// 模擬Service返回when(productService.findById(1001)).thenReturn(new Product(1001, "測試商品"));mockMvc.perform(get("/products/1001")).andExpect(jsonPath("$.name").value("測試商品"));}
}
3. Service層測試示例
@SpringBootTest
public class ProductServiceTest {@Autowiredprivate ProductService service; // 真實Service@MockBeanprivate ProductRepository repo; // 模擬Repository@Testvoid testCreateProduct() {Product product = new Product("新品");when(repo.save(any())).thenReturn(product);Product created = service.create(product);assertThat(created.getName()).isEqualTo("新品");}
}
五、高級技巧:數據準備與驗證增強
1. @Sql注解:測試數據初始化
@Test
@Sql(scripts = "/init-products.sql", // ① 初始化腳本config = @SqlConfig(transactionMode = ISOLATED))
@Sql(scripts = "/cleanup.sql", // ② 清理腳本executionPhase = AFTER_TEST_METHOD)
public void testProductCount() {int count = service.countProducts();assertThat(count).isEqualTo(10);
}
- 腳本路徑:
src/test/resources/init-products.sql
- 執行階段:
BEFORE_TEST_METHOD
(默認)或AFTER_TEST_METHOD
2. AssertJ集合斷言
List<Product> products = service.search("手機");assertThat(products).hasSize(3).extracting(Product::getPrice).allMatch(price -> price > 1000);
六、測試架構最佳實踐
1. 分層測試金字塔
- 底層測試(Service/Repository)數量最多
- 頂層測試(Controller)覆蓋關鍵接口
2. 測試數據管理策略
方式 | 適用場景 | 示例 |
---|---|---|
內存數據庫 | Repository層測試 | H2 + @DataJpaTest |
@Sql初始化 | 固定數據場景 | @Sql("/init-data.sql") |
Mockito動態生成 | 無需持久化的數據 | when(repo.findAll()).thenReturn(list) |
七、Demo 測試類(完整版)??
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@SpringBootTest
@AutoConfigureMockMvc
@Sql(scripts = "/test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) // 測試前執行SQL初始化數據[8](@ref)
@Sql(scripts = "/clean-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 測試后清理數據[8](@ref)
public class ProductControllerTest {@Autowiredprivate MockMvc mockMvc;@MockBeanprivate ProductService productService; // 模擬Service層// ========================= 增刪改查測試 =========================@Testvoid testCreateProduct() throws Exception {String jsonBody = "{\"name\":\"MacBook Pro\",\"price\":12999}";mockMvc.perform(MockMvcRequestBuilders.post("/products").contentType(MediaType.APPLICATION_JSON).content(jsonBody)).andExpect(status().isCreated()) // 斷言HTTP 201.andExpect(jsonPath("$.id").exists()) // 驗證返回的JSON有id字段[4](@ref).andDo(print()); // 打印請求/響應詳情[7](@ref)}@Testvoid testGetProductById() throws Exception {// 模擬Service返回數據when(productService.findById(1001L)).thenReturn(new Product(1001L, "iPhone 15", 7999));mockMvc.perform(MockMvcRequestBuilders.get("/products/1001")).andExpect(status().isOk()).andExpect(jsonPath("$.name").value("iPhone 15")) // JSONPath驗證字段值[1](@ref).andExpect(jsonPath("$.price").value(7999));}@Testvoid testBatchCreateProducts() throws Exception {String jsonArray = """[{"name":"iPad Air", "price":4499},{"name":"Apple Watch", "price":2999}]""";mockMvc.perform(MockMvcRequestBuilders.post("/products/batch").contentType(MediaType.APPLICATION_JSON).content(jsonArray)).andExpect(status().isCreated()).andExpect(jsonPath("$.length()").value(2)); // 驗證返回數組長度[4](@ref)}// ========================= 文件上傳測試 =========================@Testvoid testUploadProductList() throws Exception {// 構建CSV模擬文件[9,11](@ref)String csvContent = "id,name,price\n101,Keyboard,199\n102,Mouse,99";MockMultipartFile file = new MockMultipartFile("file", // 參數名(必須與@RequestParam一致)"products.csv", // 文件名"text/csv", // 文件類型csvContent.getBytes() // 文件內容);mockMvc.perform(MockMvcRequestBuilders.multipart("/products/upload").file(file).param("source", "excel")) // 附加普通參數.andExpect(status().isOk()).andExpect(content().string("2 records imported"));}// ========================= 刪除測試 =========================@Testvoid testDeleteProduct() throws Exception {mockMvc.perform(MockMvcRequestBuilders.delete("/products/1001")).andExpect(status().isNoContent()); // 204狀態碼[1](@ref)}
}
總結
通過MockMvc實現Controller層隔離測試,配合分層策略和AssertJ斷言,可構建高效的測試體系。關鍵實踐:
- 使用
@WebMvcTest + MockMvc
測試Controller,不啟動Web服務器 - Service層用
@SpringBootTest + @MockBean
進行集成測試 - 利用JSONPath高效驗證復雜JSON響應
- 通過
@Sql
管理測試數據生命周期
避坑指南:避免在Controller測試中加載完整上下文(
@SpringBootTest
),否則會導致測試速度下降10倍以上
實戰項目源碼下載 | Spring官方測試指南