SpringBoot整合Mockito進(jìn)行單元測(cè)試實(shí)踐
一、環(huán)境準(zhǔn)備
1. 添加 Maven 依賴(lài)
<dependencies> <!-- Spring Boot 測(cè)試核心依賴(lài) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Mockito 核心庫(kù) --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <scope>test</scope> </dependency> <!-- 用于斷言的實(shí)用工具 --> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency> <!-- JSON 路徑驗(yàn)證(用于 Controller 測(cè)試) --> <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path-assert</artifactId> <scope>test</scope> </dependency> </dependencies>
二、核心概念與工具
1.JUnit 5 注解
| 注解 | 用途 |
|---|---|
| @Test | 標(biāo)記測(cè)試方法 |
| @BeforeEach | 每個(gè)測(cè)試方法執(zhí)行前調(diào)用 |
| @AfterEach | 每個(gè)測(cè)試方法執(zhí)行后調(diào)用 |
| @DisplayName | 為測(cè)試方法設(shè)置可讀名稱(chēng) |
| @ParameterizedTest | 支持參數(shù)化測(cè)試(多組輸入測(cè)試同一邏輯) |
2.Mockito 注解
| 注解 | 用途 |
|---|---|
| @Mock | 創(chuàng)建模擬對(duì)象(非 Spring 容器管理) |
| @InjectMocks | 自動(dòng)注入被測(cè)試類(lèi)(配合 @Mock 使用) |
| @MockBean | 在 Spring 容器中創(chuàng)建模擬 Bean(適用于 @SpringBootTest) |
| @Spy | 創(chuàng)建部分模擬對(duì)象(真實(shí)方法調(diào)用 + 指定方法模擬) |
三、實(shí)戰(zhàn)案例詳解
案例 1:純 Java 服務(wù)類(lèi)測(cè)試(無(wú) Spring 上下文)
1. 被測(cè)試類(lèi)
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
2. 單元測(cè)試
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
@DisplayName("測(cè)試獲取用戶(hù) - 用戶(hù)存在")
void testGetUserById_UserExists() {
// 1. 模擬 Repository 行為
User mockUser = new User(1L, "Alice", "alice@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// 2. 執(zhí)行測(cè)試方法
User result = userService.getUserById(1L);
// 3. 驗(yàn)證結(jié)果
assertNotNull(result);
assertEquals("Alice", result.getName());
assertEquals("alice@example.com", result.getEmail());
// 4. 驗(yàn)證方法調(diào)用
verify(userRepository, times(1)).findById(1L);
}
@Test
@DisplayName("測(cè)試獲取用戶(hù) - 用戶(hù)不存在")
void testGetUserById_UserNotExists() {
// 1. 模擬 Repository 行為
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// 2. 執(zhí)行測(cè)試方法
User result = userService.getUserById(999L);
// 3. 驗(yàn)證結(jié)果
assertNull(result);
verify(userRepository, times(1)).findById(999L);
}
}
案例 2:Spring Boot Controller 層測(cè)試(使用 MockMvc)
1. 被測(cè)試類(lèi)
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
}
2. 單元測(cè)試
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
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.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(UserController.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
@DisplayName("測(cè)試獲取用戶(hù) - 用戶(hù)存在")
void testGetUserById_UserExists() throws Exception {
// 1. 模擬 Service 返回值
User mockUser = new User(1L, "Alice", "alice@example.com");
when(userService.getUserById(1L)).thenReturn(mockUser);
// 2. 發(fā)起 HTTP 請(qǐng)求
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.name").value("Alice"))
.andExpect(jsonPath("$.email").value("alice@example.com"))
.andDo(print());
}
@Test
@DisplayName("測(cè)試獲取用戶(hù) - 用戶(hù)不存在")
void testGetUserById_UserNotExists() throws Exception {
// 1. 模擬 Service 返回值
when(userService.getUserById(999L)).thenReturn(null);
// 2. 發(fā)起 HTTP 請(qǐng)求
mockMvc.perform(get("/users/999"))
.andExpect(status().isNotFound());
}
}
案例 3:Repository 層測(cè)試(使用內(nèi)存數(shù)據(jù)庫(kù) H2)
1. 配置 H2 內(nèi)存數(shù)據(jù)庫(kù)
@DataJpaTest
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("測(cè)試保存用戶(hù)")
void testSaveUser() {
// 1. 準(zhǔn)備測(cè)試數(shù)據(jù)
User user = new User(null, "Bob", "bob@example.com");
User savedUser = userRepository.save(user);
// 2. 驗(yàn)證結(jié)果
assertNotNull(savedUser.getId());
assertEquals("Bob", savedUser.getName());
assertEquals("bob@example.com", savedUser.getEmail());
// 3. 驗(yàn)證數(shù)據(jù)庫(kù)記錄
User foundUser = userRepository.findById(savedUser.getId()).orElse(null);
assertNotNull(foundUser);
assertEquals("Bob", foundUser.getName());
}
@Test
@DisplayName("測(cè)試查詢(xún)用戶(hù)")
void testFindByEmail() {
// 1. 準(zhǔn)備測(cè)試數(shù)據(jù)
User user = new User(null, "Charlie", "charlie@example.com");
userRepository.save(user);
// 2. 查詢(xún)驗(yàn)證
User result = userRepository.findByEmail("charlie@example.com");
assertNotNull(result);
assertEquals("Charlie", result.getName());
assertEquals("charlie@example.com", result.getEmail());
}
}
案例 4:Service 層測(cè)試(結(jié)合異常場(chǎng)景)
1. 被測(cè)試類(lèi)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException("User not found"));
}
}
2. 單元測(cè)試
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
@DisplayName("測(cè)試獲取用戶(hù) - 拋出異常")
void testGetUserById_ThrowsException() {
// 1. 模擬 Repository 行為
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// 2. 執(zhí)行測(cè)試并捕獲異常
assertThrows(UserNotFoundException.class, () -> {
userService.getUserById(999L);
});
// 3. 驗(yàn)證方法調(diào)用
verify(userRepository, times(1)).findById(999L);
}
}
四、Mockito 高級(jí)用法
1.模擬任意參數(shù)
when(userService.getUserById(anyLong())).thenReturn(new User(1L, "Test User"));
2.參數(shù)捕獲與驗(yàn)證
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals("Alice", capturedUser.getName());
3.部分模擬(Spy)
@Spy
private UserService userService;
@Test
void testSpy() {
doReturn(new User(1L, "Mock User")).when(userService).getUserById(anyLong());
User result = userService.getUserById(1L);
assertEquals("Mock User", result.getName());
}
4.BDD 風(fēng)格測(cè)試
@Test
void testBddStyle() {
// Given
User mockUser = new User(1L, "Alice");
when(userService.getUserById(1L)).thenReturn(mockUser);
// When
User result = userService.getUserById(1L);
// Then
assertNotNull(result);
assertEquals("Alice", result.getName());
}
五、測(cè)試覆蓋率與報(bào)告
1.Jacoco 覆蓋率報(bào)告
1.1 添加依賴(lài)
<dependency> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> </dependency>
1.2 執(zhí)行測(cè)試并生成報(bào)告
mvn clean test mvn jacoco:report
1.3 查看報(bào)告
報(bào)告路徑:target/site/jacoco/index.html
六、常見(jiàn)問(wèn)題與解決方案
1.問(wèn)題:@Mock注解未生效
- 原因:未調(diào)用
MockitoAnnotations.openMocks(this) - 解決方案:在
@BeforeEach方法中初始化
2.問(wèn)題:Spring 上下文未加載
- 原因:未使用
@SpringBootTest - 解決方案:使用
@SpringBootTest或更細(xì)粒度的測(cè)試注解(如@WebMvcTest)
3.問(wèn)題:測(cè)試方法執(zhí)行順序異常
- 原因:JUnit 5 默認(rèn)按字母順序執(zhí)行測(cè)試方法
- 解決方案:使用
@Order注解或@TestMethodOrder
4.問(wèn)題:測(cè)試數(shù)據(jù)庫(kù)污染
- 原因:測(cè)試數(shù)據(jù)未清理
- 解決方案:使用
@DirtiesContext或手動(dòng)清理數(shù)據(jù)
七、總結(jié)
| 測(cè)試類(lèi)型 | 推薦注解 | 適用場(chǎng)景 |
|---|---|---|
| 純 Java 服務(wù)類(lèi) | @Mock + @InjectMocks | 無(wú) Spring 上下文的單元測(cè)試 |
| Spring Controller | @WebMvcTest + @MockBean | HTTP 接口測(cè)試 |
| Repository 層 | @DataJpaTest | 數(shù)據(jù)庫(kù)操作驗(yàn)證 |
| 集成測(cè)試 | @SpringBootTest | 多組件協(xié)作驗(yàn)證 |
八、擴(kuò)展學(xué)習(xí)
- 測(cè)試框架對(duì)比:JUnit 5 vs TestNG
- 測(cè)試覆蓋率工具:Jacoco、Clover
- 測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD):先寫(xiě)測(cè)試再寫(xiě)代碼
- 持續(xù)集成(CI):GitHub Actions + Test Coverage
測(cè)試原則:
- 單元測(cè)試:關(guān)注單一職責(zé),快速失敗,無(wú)需外部依賴(lài)
- 集成測(cè)試:驗(yàn)證組件協(xié)作,模擬真實(shí)環(huán)境
- 測(cè)試覆蓋率:追求 80%+ 核心邏輯覆蓋,避免過(guò)度測(cè)試
測(cè)試目標(biāo):
- 所有邊界條件都被覆蓋
- 所有異常場(chǎng)景都被模擬
- 所有業(yè)務(wù)邏輯都有驗(yàn)證
通過(guò)本教程,您應(yīng)能全面掌握 Spring Boot 與 Mockito 的單元測(cè)試技巧,并能夠編寫(xiě)高效、可靠的測(cè)試代碼。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java 高并發(fā)二:多線程基礎(chǔ)詳細(xì)介紹
本文主要介紹Java 高并發(fā)多線程的知識(shí),這里整理詳細(xì)的資料來(lái)解釋線程的知識(shí),有需要的學(xué)習(xí)高并發(fā)的朋友可以參考下2016-09-09
史上最全Java8日期時(shí)間工具類(lèi)(分享)
這篇文章主要介紹了史上最全Java8日期時(shí)間工具類(lèi)(分享),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
手把手帶你分析SpringBoot自動(dòng)裝配完成了Ribbon哪些核心操作
這篇文章主要介紹了詳解Spring Boot自動(dòng)裝配Ribbon哪些核心操作的哪些操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-08-08
基于SpringBoot實(shí)現(xiàn)圖片上傳與顯示
這篇文章主要為大家詳細(xì)介紹了基于SpringBoot實(shí)現(xiàn)圖片上傳與顯示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
SpringBoot實(shí)現(xiàn)HTTP調(diào)用的七種方式總結(jié)
小編在工作中,遇到一些需要調(diào)用三方接口的任務(wù),就需要用到 HTTP 調(diào)用工具,這里,我總結(jié)了一下 實(shí)現(xiàn) HTTP 調(diào)用的方式,共有 7 種(后續(xù)會(huì)繼續(xù)新增),需要的朋友可以參考下2023-09-09
分布式醫(yī)療掛號(hào)系統(tǒng)SpringCache與Redis為數(shù)據(jù)字典添加緩存
這篇文章主要為大家介紹了分布式醫(yī)療掛號(hào)系統(tǒng)SpringCache與Redis為數(shù)據(jù)字典添加緩存,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04

