在 Java 项目中,JUnit、Mockito、MockMvc 和 PowerMock 经常同时出现,但它们并不是四个功能重复的测试框架。
它们分别解决不同层次的问题:
JUnit :组织和运行测试。
Mockito :创建依赖替身,控制依赖行为并验证调用。
MockMvc :模拟 Spring MVC 请求链路,测试 Controller、参数绑定、序列化和异常处理。
PowerMock :处理静态方法、构造器、私有方法等难以测试的遗留代码。
本文会从职责边界、工程配置、代码示例、常见错误和迁移策略几个方面,构建一套完整的 Java 测试方法。
本文中的 PowerMock 示例使用 JUnit 4 + Mockito 2 + PowerMock 2.0.9 。这是由 PowerMock 的技术路线和兼容性决定的,并不代表新项目应继续采用这套组合。
一、先理解四个框架分别负责什么 1. JUnit:测试运行框架 JUnit 负责:
发现测试类和测试方法;
管理测试生命周期;
执行断言;
输出测试结果;
与 Maven、Gradle、IDE、CI 平台集成。
JUnit 本身不会自动创建业务依赖的 Mock,也不会模拟 HTTP 请求。
典型代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import org.junit.Test;import static org.junit.Assert.assertEquals;public class CalculatorTest { @Test public void shouldAddTwoNumbers () { Calculator calculator = new Calculator (); int result = calculator.add(1 , 2 ); assertEquals(3 , result); } }
JUnit 解决的是:测试怎样被定义、组织和执行。
2. Mockito:依赖隔离框架 Mockito 主要用于创建测试替身:
1 UserRepository userRepository = Mockito.mock(UserRepository.class);
然后可以指定 Mock 的行为:
1 2 Mockito.when (userRepository.findById(1L )) .thenReturn(Optional.of(user));
也可以验证调用:
1 Mockito.verify(userRepository).findById(1L );
Mockito 解决的是:被测试对象依赖了数据库、RPC、缓存、消息队列或其他服务时,怎样把这些外部依赖隔离掉。
3. MockMvc:Spring MVC 请求模拟器 MockMvc 用于测试 Spring MVC Web 层。它可以在不启动真实 Servlet 容器、不监听真实端口的情况下,执行完整的 MVC 请求处理流程。
它可以覆盖:
URL 路由;
HTTP 方法;
请求参数;
PathVariable;
RequestBody;
JSON 序列化与反序列化;
ControllerAdvice;
HTTP 状态码;
响应头;
响应 JSON;
参数校验;
Filter 和 Interceptor 的部分场景。
典型调用如下:
1 2 3 mockMvc.perform(get("/api/users/1" )) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.id" ).value(1 ));
MockMvc 解决的是:Controller 在 Spring MVC 链路中的行为是否正确。
4. PowerMock:遗留代码的强制隔离工具 PowerMock 通过自定义类加载器和字节码修改,提供以下能力:
Mock 静态方法;
Mock 构造器;
Mock final 类或 final 方法;
Mock 私有方法;
跳过静态初始化块;
修改内部状态。
例如:
1 2 3 4 PowerMockito.mockStatic(LegacySecurityContext.class); Mockito.when (LegacySecurityContext.currentOperator()) .thenReturn("admin" );
PowerMock 解决的是:代码本身不具备可测试性,但暂时又不能立即重构时,怎样先建立测试保护网。
它更像一把“破拆锤”,而不是日常拧螺丝的工具。锤子很好用,但最好不要拿它修手表。
二、四个框架的协作关系 graph TD
A[JUnit 测试运行器] --> B[测试生命周期]
A --> C[断言与测试结果]
B --> D[Mockito]
B --> E[MockMvc]
B --> F[PowerMock]
D --> G[Mock 普通依赖]
D --> H[Stub 返回值]
D --> I[Verify 调用行为]
E --> J[模拟 HTTP 请求]
E --> K[执行 Spring MVC 链路]
E --> L[校验状态码和 JSON]
F --> M[Mock 静态方法]
F --> N[Mock new 构造器]
F --> O[处理遗留代码]
从测试分层看,可以这样理解:
测试目标
主要工具
是否启动 Spring 上下文
是否启动真实服务器
普通 Java 类
JUnit + Mockito
否
否
Controller 单元测试
JUnit + Mockito + MockMvc standalone
否
否
Spring MVC 切片测试
JUnit + MockMvc + Spring Test
部分
否
遗留静态代码
JUnit 4 + Mockito + PowerMock
通常否
否
完整接口集成测试
Spring Test/Testcontainers/真实依赖
是
可选
三、测试金字塔中的位置 graph BT
A[大量:普通单元测试<br/>JUnit + Mockito] --> B[适量:Web 层测试<br/>MockMvc]
B --> C[少量:集成测试<br/>Spring Context + DB/MQ]
C --> D[极少量:端到端测试<br/>真实服务和网络]
推荐原则:
业务分支尽量在普通单元测试中覆盖;
HTTP 协议、路由、参数绑定、异常映射使用 MockMvc 覆盖;
数据库 SQL、事务、消息队列等使用集成测试覆盖;
PowerMock 只处理暂时无法重构的遗留边界;
不要把所有逻辑都塞进 @SpringBootTest。
测试越靠近金字塔底部,通常运行越快、定位问题越容易、维护成本越低。
四、版本兼容性:这是最容易踩坑的地方 PowerMock 会调用 Mockito 的内部 API,并通过自己的类加载器修改字节码,因此版本兼容性远弱于普通测试库。
本文采用以下组合:
组件
示例版本
说明
JDK
8 或 11
PowerMock 在较新 JDK 上容易遇到模块访问问题
JUnit
4.13.2
PowerMockRunner 基于 JUnit 4
Mockito
2.28.2
与 PowerMock 2.x 相对稳定的组合
PowerMock
2.0.9
PowerMock 的遗留稳定版本
Spring Framework
5.3.x
使用 javax.servlet 命名空间
Jackson
2.x
用于 MockMvc JSON 序列化
需要特别注意 截至 2026 年,JUnit 的当前主线已经进入 JUnit 6,Mockito 当前主线为 Mockito 5,Spring Framework 当前主线也已经进入 7.x。但 PowerMock 仍然停留在较老的技术栈中。
因此:
新项目不要主动引入 PowerMock;
已经使用 PowerMock 的项目,应固定依赖版本;
不要让 Spring Boot 的依赖管理自动把 Mockito 升级到不兼容的大版本;
JDK 17、21 或更高版本项目,应优先通过重构或 Mockito 原生能力替代 PowerMock;
PowerMock 测试最好与现代 JUnit 5/6 测试分模块或分目录管理。
五、Maven 项目配置 下面给出一个适用于本文示例的 pom.xml。为了避免 Spring Boot 自动管理 Mockito 版本,这里直接依赖 Spring Framework 测试组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.example</groupId > <artifactId > java-testing-demo</artifactId > <version > 1.0.0-SNAPSHOT</version > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <spring.version > 5.3.39</spring.version > <jackson.version > 2.17.2</jackson.version > <junit.version > 4.13.2</junit.version > <mockito.version > 2.28.2</mockito.version > <powermock.version > 2.0.9</powermock.version > </properties > <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-webmvc</artifactId > <version > ${spring.version}</version > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > <version > ${jackson.version}</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-test</artifactId > <version > ${spring.version}</version > <scope > test</scope > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 4.0.1</version > <scope > provided</scope > </dependency > <dependency > <groupId > com.jayway.jsonpath</groupId > <artifactId > json-path</artifactId > <version > 2.9.0</version > <scope > test</scope > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > ${junit.version}</version > <scope > test</scope > </dependency > <dependency > <groupId > org.mockito</groupId > <artifactId > mockito-core</artifactId > <version > ${mockito.version}</version > <scope > test</scope > </dependency > <dependency > <groupId > org.powermock</groupId > <artifactId > powermock-module-junit4</artifactId > <version > ${powermock.version}</version > <scope > test</scope > </dependency > <dependency > <groupId > org.powermock</groupId > <artifactId > powermock-api-mockito2</artifactId > <version > ${powermock.version}</version > <scope > test</scope > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-surefire-plugin</artifactId > <version > 2.22.2</version > </plugin > </plugins > </build > </project >
查看依赖树:
重点检查:
1 2 3 4 org.mockito:mockito-core org.powermock:powermock-api-mockito2 org.powermock:powermock-module-junit4 junit:junit
如果项目中同时出现多个 Mockito 大版本,测试很容易在启动阶段报 NoSuchMethodError、ClassNotFoundException 或 IllegalAccessError。
六、示例项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 src ├── main │ └── java │ └── com.example.testing │ ├── controller │ │ ├── UserController.java │ │ └── GlobalExceptionHandler.java │ ├── domain │ │ ├── UserEntity.java │ │ ├── UserDto.java │ │ ├── CreateUserRequest.java │ │ └── ApiResponse.java │ ├── exception │ │ ├── UserNotFoundException.java │ │ └── DuplicateUsernameException.java │ ├── legacy │ │ ├── LegacySecurityContext.java │ │ ├── LegacyPdfClient.java │ │ └── LegacyReportService.java │ ├── repository │ │ └── UserRepository.java │ └── service │ ├── UserService.java │ └── UserApplicationService.java └── test └── java └── com.example.testing ├── controller │ ├── UserControllerMockMvcTest.java │ └── UserControllerPowerMockTest.java ├── legacy │ └── LegacyReportServiceTest.java └── service └── UserApplicationServiceTest.java
七、准备业务代码 1. UserEntity 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.example.testing.domain;public class UserEntity { private Long id; private String username; private String displayName; public UserEntity () { } public UserEntity (Long id, String username, String displayName) { this .id = id; this .username = username; this .displayName = displayName; } public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public String getDisplayName () { return displayName; } public void setDisplayName (String displayName) { this .displayName = displayName; } }
2. UserDto 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.example.testing.domain;public class UserDto { private Long id; private String username; private String displayName; public UserDto () { } public UserDto (Long id, String username, String displayName) { this .id = id; this .username = username; this .displayName = displayName; } public Long getId () { return id; } public String getUsername () { return username; } public String getDisplayName () { return displayName; } }
3. CreateUserRequest 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.example.testing.domain;public class CreateUserRequest { private String username; private String displayName; public CreateUserRequest () { } public CreateUserRequest (String username, String displayName) { this .username = username; this .displayName = displayName; } public String getUsername () { return username; } public String getDisplayName () { return displayName; } }
4. ApiResponse 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.example.testing.domain;public class ApiResponse <T> { private final boolean success; private final String message; private final T data; private ApiResponse (boolean success, String message, T data) { this .success = success; this .message = message; this .data = data; } public static <T> ApiResponse<T> success (T data) { return new ApiResponse <>(true , "OK" , data); } public static <T> ApiResponse<T> failure (String message) { return new ApiResponse <>(false , message, null ); } public boolean isSuccess () { return success; } public String getMessage () { return message; } public T getData () { return data; } }
5. Repository 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.example.testing.repository;import com.example.testing.domain.UserEntity;import java.util.Optional;public interface UserRepository { Optional<UserEntity> findById (Long id) ; boolean existsByUsername (String username) ; UserEntity save (UserEntity entity) ; }
6. 异常类 1 2 3 4 5 6 7 8 package com.example.testing.exception;public class UserNotFoundException extends RuntimeException { public UserNotFoundException (Long id) { super ("User not found: " + id); } }
1 2 3 4 5 6 7 8 package com.example.testing.exception;public class DuplicateUsernameException extends RuntimeException { public DuplicateUsernameException (String username) { super ("Username already exists: " + username); } }
7. Service 接口 1 2 3 4 5 6 7 8 9 10 11 package com.example.testing.service;import com.example.testing.domain.CreateUserRequest;import com.example.testing.domain.UserDto;public interface UserService { UserDto findById (Long id) ; UserDto create (CreateUserRequest request) ; }
8. Service 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.example.testing.service;import com.example.testing.domain.CreateUserRequest;import com.example.testing.domain.UserDto;import com.example.testing.domain.UserEntity;import com.example.testing.exception.DuplicateUsernameException;import com.example.testing.exception.UserNotFoundException;import com.example.testing.repository.UserRepository;public class UserApplicationService implements UserService { private final UserRepository userRepository; public UserApplicationService (UserRepository userRepository) { this .userRepository = userRepository; } @Override public UserDto findById (Long id) { UserEntity entity = userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException (id)); return toDto(entity); } @Override public UserDto create (CreateUserRequest request) { if (userRepository.existsByUsername(request.getUsername())) { throw new DuplicateUsernameException (request.getUsername()); } UserEntity entity = new UserEntity ( null , request.getUsername(), request.getDisplayName() ); UserEntity saved = userRepository.save(entity); return toDto(saved); } private UserDto toDto (UserEntity entity) { return new UserDto ( entity.getId(), entity.getUsername(), entity.getDisplayName() ); } }
9. 遗留静态上下文 1 2 3 4 5 6 7 8 9 10 11 12 package com.example.testing.legacy;public final class LegacySecurityContext { private LegacySecurityContext () { } public static String currentOperator () { throw new IllegalStateException ("No operator in current thread" ); } }
10. Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.example.testing.controller;import com.example.testing.domain.ApiResponse;import com.example.testing.domain.CreateUserRequest;import com.example.testing.domain.UserDto;import com.example.testing.legacy.LegacySecurityContext;import com.example.testing.service.UserService;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController (UserService userService) { this .userService = userService; } @GetMapping("/{id}") public ApiResponse<UserDto> findById (@PathVariable Long id) { return ApiResponse.success(userService.findById(id)); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public ApiResponse<UserDto> create (@RequestBody CreateUserRequest request) { return ApiResponse.success(userService.create(request)); } @GetMapping("/{id}/audit") public ApiResponse<String> audit (@PathVariable Long id) { String operator = LegacySecurityContext.currentOperator(); UserDto user = userService.findById(id); return ApiResponse.success(operator + " accessed " + user.getUsername()); } }
11. 全局异常处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.example.testing.controller;import com.example.testing.domain.ApiResponse;import com.example.testing.exception.DuplicateUsernameException;import com.example.testing.exception.UserNotFoundException;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(UserNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ApiResponse<Void> handleUserNotFound (UserNotFoundException exception) { return ApiResponse.failure(exception.getMessage()); } @ExceptionHandler(DuplicateUsernameException.class) @ResponseStatus(HttpStatus.CONFLICT) public ApiResponse<Void> handleDuplicateUsername ( DuplicateUsernameException exception) { return ApiResponse.failure(exception.getMessage()); } }
八、JUnit + Mockito:测试业务 Service 1. 基础测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 package com.example.testing.service;import com.example.testing.domain.CreateUserRequest;import com.example.testing.domain.UserDto;import com.example.testing.domain.UserEntity;import com.example.testing.exception.DuplicateUsernameException;import com.example.testing.exception.UserNotFoundException;import com.example.testing.repository.UserRepository;import org.junit.Test;import org.junit.runner.RunWith;import org.mockito.ArgumentCaptor;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.junit.MockitoJUnitRunner;import java.util.Optional;import static org.junit.Assert.assertEquals;import static org.junit.Assert.assertNull;import static org.junit.Assert.assertThrows;import static org.mockito.Mockito.never;import static org.mockito.Mockito.verify;import static org.mockito.Mockito.verifyNoMoreInteractions;import static org.mockito.Mockito.when ;@RunWith(MockitoJUnitRunner.class) public class UserApplicationServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserApplicationService userApplicationService; @Test public void shouldReturnUserWhenUserExists () { UserEntity entity = new UserEntity (1L , "mario" , "Super Mario" ); when (userRepository.findById(1L )) .thenReturn(Optional.of(entity)); UserDto result = userApplicationService.findById(1L ); assertEquals(Long.valueOf(1L ), result.getId()); assertEquals("mario" , result.getUsername()); assertEquals("Super Mario" , result.getDisplayName()); verify(userRepository).findById(1L ); verifyNoMoreInteractions(userRepository); } @Test public void shouldThrowExceptionWhenUserDoesNotExist () { when (userRepository.findById(99L )) .thenReturn(Optional.empty()); UserNotFoundException exception = assertThrows( UserNotFoundException.class, () -> userApplicationService.findById(99L ) ); assertEquals("User not found: 99" , exception.getMessage()); verify(userRepository).findById(99L ); } @Test public void shouldCreateUserAndCaptureSavedEntity () { CreateUserRequest request = new CreateUserRequest ( "mario" , "Super Mario" ); when (userRepository.existsByUsername("mario" )) .thenReturn(false ); when (userRepository.save(org.mockito.ArgumentMatchers.any(UserEntity.class))) .thenAnswer(invocation -> { UserEntity entity = invocation.getArgument(0 ); entity.setId(100L ); return entity; }); UserDto result = userApplicationService.create(request); ArgumentCaptor<UserEntity> captor = ArgumentCaptor.forClass(UserEntity.class); verify(userRepository).save(captor.capture()); UserEntity savedArgument = captor.getValue(); assertNull(savedArgument.getId()); assertEquals("mario" , savedArgument.getUsername()); assertEquals("Super Mario" , savedArgument.getDisplayName()); assertEquals(Long.valueOf(100L ), result.getId()); assertEquals("mario" , result.getUsername()); } @Test public void shouldRejectDuplicateUsername () { CreateUserRequest request = new CreateUserRequest ( "mario" , "Another Mario" ); when (userRepository.existsByUsername("mario" )) .thenReturn(true ); DuplicateUsernameException exception = assertThrows( DuplicateUsernameException.class, () -> userApplicationService.create(request) ); assertEquals( "Username already exists: mario" , exception.getMessage() ); verify(userRepository).existsByUsername("mario" ); verify(userRepository, never()) .save(org.mockito.ArgumentMatchers.any(UserEntity.class)); } }
2. @Mock 与 @InjectMocks @Mock 创建测试依赖:
1 2 @Mock private UserRepository userRepository;
@InjectMocks 创建被测试对象,并尝试注入 Mock:
1 2 @InjectMocks private UserApplicationService userApplicationService;
Mockito 的注入顺序大致包括:
构造器注入;
Setter 注入;
字段注入。
生产代码建议优先使用构造器注入,这样依赖明确,也更容易测试。
3. when(...).thenReturn(...) 1 2 when (userRepository.findById(1L )) .thenReturn(Optional.of(entity));
含义是:当调用 findById(1L) 时,不访问真实数据库,而是返回预先准备的数据。
4. thenAnswer(...) 当返回值需要根据入参动态生成时,可以使用 thenAnswer:
1 2 3 4 5 6 when (userRepository.save(any(UserEntity.class))) .thenAnswer(invocation -> { UserEntity entity = invocation.getArgument(0 ); entity.setId(100L ); return entity; });
它适合模拟:
数据库生成主键;
RPC 根据请求内容返回结果;
不同参数对应不同返回值;
延迟或异常行为。
5. verify(...) 1 verify(userRepository).findById(1L );
验证的是行为,而不是返回值。
常见形式:
1 2 3 4 verify(repository, times(1 )).save(any()); verify(repository, never()).delete(any()); verify(repository, atLeastOnce()).findById(anyLong()); verify(repository, atMost(2 )).findById(anyLong());
不要把每一个内部调用都写成 verify。过度验证会让测试和实现细节强绑定,稍微重构就全线变红。
6. ArgumentCaptor ArgumentCaptor 用于捕获传给依赖的实际参数:
1 2 3 4 5 6 7 ArgumentCaptor<UserEntity> captor = ArgumentCaptor.forClass(UserEntity.class); verify(userRepository).save(captor.capture());UserEntity value = captor.getValue(); assertEquals("mario" , value.getUsername());
适合验证:
DTO 到 Entity 的转换;
消息内容;
RPC 请求对象;
数据库保存对象;
审计字段。
九、Mockito 常用能力 1. Mock 完全模拟对象:
1 UserRepository repository = mock(UserRepository.class);
没有配置的方法通常返回默认值:
对象返回 null;
boolean 返回 false;
数字返回 0;
集合在部分 API 中返回空集合;
Optional 在较新 Mockito 中通常返回空 Optional。
2. Spy Spy 包装一个真实对象,未 Stub 的方法会执行真实逻辑:
1 2 3 4 5 List<String> list = spy(new ArrayList <>()); list.add("A" ); assertEquals(1 , list.size());
Stub Spy 时优先使用:
1 doReturn(100 ).when (spyObject).calculate();
而不是:
1 when (spyObject.calculate()).thenReturn(100 );
后者在 Stub 阶段可能先执行一次真实方法。
3. void 方法 1 doNothing().when (messageProducer).send(any());
模拟异常:
1 2 3 doThrow(new RuntimeException ("send failed" )) .when (messageProducer) .send(any());
4. 连续返回 1 2 3 4 when (remoteClient.query()) .thenReturn("first" ) .thenReturn("second" ) .thenThrow(new RuntimeException ("third call failed" ));
5. 参数匹配器 1 2 3 4 when (repository.findByTypeAndStatus( eq("ORDER" ), anyInt() )).thenReturn(Collections.emptyList());
一旦一个参数使用了 Matcher,同一次调用中的其他参数也应使用 Matcher:
错误写法:
1 2 when (repository.findByTypeAndStatus("ORDER" , anyInt())) .thenReturn(Collections.emptyList());
正确写法:
1 2 when (repository.findByTypeAndStatus(eq("ORDER" ), anyInt())) .thenReturn(Collections.emptyList());
否则容易出现:
1 InvalidUseOfMatchersException
十、JUnit + Mockito + MockMvc:测试 Controller 1. 使用 standaloneSetup standaloneSetup 只注册指定 Controller,不启动完整 Spring 上下文,速度很快,适合测试:
URL 映射;
请求参数;
RequestBody;
HTTP 状态码;
JSON 响应;
ControllerAdvice;
Controller 与 Service 的协作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 package com.example.testing.controller;import com.example.testing.domain.CreateUserRequest;import com.example.testing.domain.UserDto;import com.example.testing.exception.UserNotFoundException;import com.example.testing.service.UserService;import com.fasterxml.jackson.databind.ObjectMapper;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.junit.MockitoJUnitRunner;import org.springframework.http.MediaType;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import static org.mockito.ArgumentMatchers.any;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.request.MockMvcRequestBuilders.post;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;@RunWith(MockitoJUnitRunner.class) public class UserControllerMockMvcTest { @Mock private UserService userService; @InjectMocks private UserController userController; private MockMvc mockMvc; private ObjectMapper objectMapper; @Before public void setUp () { objectMapper = new ObjectMapper (); mockMvc = MockMvcBuilders .standaloneSetup(userController) .setControllerAdvice(new GlobalExceptionHandler ()) .build(); } @Test public void shouldReturnUser () throws Exception { UserDto user = new UserDto (1L , "mario" , "Super Mario" ); when (userService.findById(1L )).thenReturn(user); mockMvc.perform(get("/api/users/{id}" , 1L ) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith( MediaType.APPLICATION_JSON )) .andExpect(jsonPath("$.success" ).value(true )) .andExpect(jsonPath("$.message" ).value("OK" )) .andExpect(jsonPath("$.data.id" ).value(1 )) .andExpect(jsonPath("$.data.username" ).value("mario" )) .andExpect(jsonPath("$.data.displayName" ) .value("Super Mario" )); verify(userService).findById(1L ); } @Test public void shouldReturn404WhenUserDoesNotExist () throws Exception { when (userService.findById(99L )) .thenThrow(new UserNotFoundException (99L )); mockMvc.perform(get("/api/users/{id}" , 99L ) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.success" ).value(false )) .andExpect(jsonPath("$.message" ) .value("User not found: 99" )) .andExpect(jsonPath("$.data" ).doesNotExist()); } @Test public void shouldCreateUser () throws Exception { CreateUserRequest request = new CreateUserRequest ( "mario" , "Super Mario" ); UserDto created = new UserDto ( 100L , "mario" , "Super Mario" ); when (userService.create(any(CreateUserRequest.class))) .thenReturn(created); mockMvc.perform(post("/api/users" ) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.success" ).value(true )) .andExpect(jsonPath("$.data.id" ).value(100 )) .andExpect(jsonPath("$.data.username" ).value("mario" )); verify(userService).create(any(CreateUserRequest.class)); } }
2. MockMvc 请求执行链路 sequenceDiagram
participant T as JUnit Test
participant M as MockMvc
participant D as DispatcherServlet
participant C as UserController
participant S as Mock UserService
participant A as ResultMatcher
T->>M: perform(GET /api/users/1)
M->>D: 构造 MockHttpServletRequest
D->>C: 路由并绑定参数
C->>S: findById(1)
S-->>C: UserDto
C-->>D: ApiResponse<UserDto>
D-->>M: JSON Response
M->>A: 校验状态码、Content-Type、JSON
A-->>T: 测试成功或失败
3. 为什么 Service 仍然要 Mock MockMvc 测试 Controller 时,不应该让测试直接访问真实数据库,否则测试目标会变得模糊。
Controller 测试重点是:
URL 是否正确;
HTTP 方法是否正确;
参数是否正确绑定;
Service 返回结果后,响应是否正确;
Service 抛异常后,状态码和错误体是否正确。
数据库行为应在 Repository 集成测试中单独验证。
十一、MockMvc 的两种初始化模式 1. standaloneSetup 1 2 3 4 MockMvcBuilders .standaloneSetup(userController) .setControllerAdvice(new GlobalExceptionHandler ()) .build();
特点:
不启动 Spring 容器;
速度快;
配置清晰;
依赖通常由 Mockito 注入;
需要手动添加 ControllerAdvice、Formatter、Filter 等组件;
不能完全验证真实 Spring MVC 配置。
适合:Controller 单元测试。
2. webAppContextSetup 1 2 3 MockMvcBuilders .webAppContextSetup(webApplicationContext) .build();
特点:
基于真实 WebApplicationContext;
会加载 Spring MVC 配置;
可以验证真实的转换器、拦截器、异常处理器;
比 standalone 更慢;
测试失败时,定位范围更大。
适合:Web 层集成测试。
3. 如何选择
场景
推荐方式
只验证单个 Controller
standaloneSetup
验证 ControllerAdvice
两者均可
验证自定义 Jackson 配置
webAppContextSetup
验证 Formatter/Converter
webAppContextSetup
验证 Interceptor
webAppContextSetup
追求毫秒级反馈
standaloneSetup
Spring Boot 新项目
@WebMvcTest 通常更方便
十二、JUnit + Mockito + PowerMock:Mock 静态方法 现在测试 Controller 中的审计接口:
1 2 3 4 5 6 7 8 9 @GetMapping("/{id}/audit") public ApiResponse<String> audit (@PathVariable Long id) { String operator = LegacySecurityContext.currentOperator(); UserDto user = userService.findById(id); return ApiResponse.success( operator + " accessed " + user.getUsername() ); }
由于 LegacySecurityContext.currentOperator() 是静态方法,普通 Mockito 2 无法直接替换它,因此使用 PowerMock。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package com.example.testing.controller;import com.example.testing.domain.UserDto;import com.example.testing.legacy.LegacySecurityContext;import com.example.testing.service.UserService;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.mockito.Mock;import org.mockito.Mockito;import org.mockito.MockitoAnnotations;import org.powermock.api.mockito.PowerMockito;import org.powermock.core.classloader.annotations.PowerMockIgnore;import org.powermock.core.classloader.annotations.PrepareForTest;import org.powermock.modules.junit4.PowerMockRunner;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import static org.mockito.Mockito.times;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;@RunWith(PowerMockRunner.class) @PrepareForTest(LegacySecurityContext.class) @PowerMockIgnore({ "javax.management.*", "javax.net.ssl.*", "javax.crypto.*", "org.xml.*", "org.w3c.*" }) public class UserControllerPowerMockTest { @Mock private UserService userService; private UserController userController; private MockMvc mockMvc; @Before public void setUp () { MockitoAnnotations.initMocks(this ); userController = new UserController (userService); mockMvc = MockMvcBuilders .standaloneSetup(userController) .setControllerAdvice(new GlobalExceptionHandler ()) .build(); PowerMockito.mockStatic(LegacySecurityContext.class); } @Test public void shouldReturnAuditInformation () throws Exception { when (LegacySecurityContext.currentOperator()) .thenReturn("admin" ); when (userService.findById(1L )) .thenReturn(new UserDto ( 1L , "mario" , "Super Mario" )); mockMvc.perform(get("/api/users/{id}/audit" , 1L )) .andExpect(status().isOk()) .andExpect(jsonPath("$.success" ).value(true )) .andExpect(jsonPath("$.data" ) .value("admin accessed mario" )); PowerMockito.verifyStatic( LegacySecurityContext.class, times(1 ) ); LegacySecurityContext.currentOperator(); verify(userService).findById(1L ); } }
关键注解 @RunWith(PowerMockRunner.class)让 JUnit 4 使用 PowerMockRunner 启动测试。
1 @RunWith(PowerMockRunner.class)
@PrepareForTest告诉 PowerMock 哪些类需要被字节码修改:
1 @PrepareForTest(LegacySecurityContext.class)
遗漏后经常出现:
1 ClassNotPreparedException
@PowerMockIgnorePowerMock 自定义类加载器可能会重复加载 JDK、XML、SSL 或第三方库中的类,导致类转换异常。
1 2 3 4 5 6 7 @PowerMockIgnore({ "javax.management.*", "javax.net.ssl.*", "javax.crypto.*", "org.xml.*", "org.w3c.*" })
这不是万能配置,应根据实际错误最小化添加。忽略范围过大,也可能让本来需要 PowerMock 修改的类失效。
静态方法验证为什么要写两步 PowerMock 验证静态方法时,需要先声明验证模式,再调用一次目标静态方法:
1 2 3 4 5 PowerMockito.verifyStatic( LegacySecurityContext.class, times(1 ) ); LegacySecurityContext.currentOperator();
第二行不会再次执行业务流程,而是告诉 PowerMock:要验证的是这个静态方法。
十三、PowerMock:Mock 构造器调用 遗留代码经常在方法内部直接 new 一个客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.example.testing.legacy;public class LegacyPdfClient { private final String endpoint; public LegacyPdfClient (String endpoint) { this .endpoint = endpoint; } public byte [] generate(String content) { throw new UnsupportedOperationException ( "Call remote PDF service: " + endpoint ); } }
1 2 3 4 5 6 7 8 9 10 11 12 package com.example.testing.legacy;public class LegacyReportService { public byte [] generateUserReport(Long userId) { LegacyPdfClient client = new LegacyPdfClient ( "https://legacy-pdf.example.com" ); return client.generate("USER-" + userId); } }
普通 Mockito 无法把方法内部创建的对象替换掉。PowerMock 可以使用 whenNew:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.example.testing.legacy;import org.junit.Test;import org.junit.runner.RunWith;import org.mockito.Mockito;import org.powermock.api.mockito.PowerMockito;import org.powermock.core.classloader.annotations.PrepareForTest;import org.powermock.modules.junit4.PowerMockRunner;import static org.junit.Assert.assertArrayEquals;import static org.mockito.Mockito.verify;@RunWith(PowerMockRunner.class) @PrepareForTest(LegacyReportService.class) public class LegacyReportServiceTest { @Test public void shouldMockObjectConstruction () throws Exception { LegacyPdfClient pdfClient = Mockito.mock(LegacyPdfClient.class); byte [] expected = new byte []{1 , 2 , 3 }; PowerMockito.whenNew(LegacyPdfClient.class) .withArguments("https://legacy-pdf.example.com" ) .thenReturn(pdfClient); Mockito.when (pdfClient.generate("USER-1" )) .thenReturn(expected); LegacyReportService service = new LegacyReportService (); byte [] result = service.generateUserReport(1L ); assertArrayEquals(expected, result); verify(pdfClient).generate("USER-1" ); PowerMockito.verifyNew(LegacyPdfClient.class) .withArguments("https://legacy-pdf.example.com" ); } }
@PrepareForTest 到底写哪个类构造器 Mock 时,应准备的是执行 new 的类 :
1 @PrepareForTest(LegacyReportService.class)
而不是只写:
1 @PrepareForTest(LegacyPdfClient.class)
因为 PowerMock 需要修改的是 LegacyReportService 中调用 new LegacyPdfClient(...) 的字节码。
十四、PowerMock:Mock 私有方法 PowerMock 可以通过字符串方法名 Mock 私有方法:
1 2 3 4 LegacyService spy = PowerMockito.spy(new LegacyService ()); PowerMockito.doReturn("mock-signature" ) .when (spy, "calculateSignature" , "payload" );
也可以验证私有方法:
1 2 PowerMockito.verifyPrivate(spy) .invoke("calculateSignature" , "payload" );
但不建议把它当作常规写法。
如果一个私有方法复杂到必须被单独 Mock,通常说明:
类承担了过多职责;
私有逻辑应提取成独立组件;
隐藏依赖没有显式建模;
测试正在绑定实现细节,而不是外部行为。
更合理的处理是把复杂私有逻辑提取到可注入的协作者中:
1 2 3 4 5 6 7 public class SignatureCalculator { public String calculate (String payload) { return "..." ; } }
然后在业务类中构造器注入:
1 2 3 4 5 6 7 8 public class PaymentService { private final SignatureCalculator signatureCalculator; public PaymentService (SignatureCalculator signatureCalculator) { this .signatureCalculator = signatureCalculator; } }
这样普通 Mockito 就足够了。
十五、不要强行把 PowerMock 和 SpringRunner 塞进一个测试类 JUnit 4 的一个测试类只能直接指定一个 Runner:
1 @RunWith(SpringRunner.class)
或者:
1 @RunWith(PowerMockRunner.class)
不能同时写两个 @RunWith。
如果确实需要同时加载 Spring 上下文并使用 PowerMock,可以使用 Runner Delegate:
1 2 3 4 5 6 @RunWith(PowerMockRunner.class) @PowerMockRunnerDelegate(SpringRunner.class) @PrepareForTest(LegacySecurityContext.class) @ContextConfiguration(classes = TestConfiguration.class) public class LegacySpringIntegrationTest { }
但这种组合具有明显缺点:
启动慢;
类加载问题更多;
调试困难;
对框架版本敏感;
容易和 JaCoCo、Mockito Inline、Spring 代理冲突。
更推荐的方式是拆分测试:
1 2 3 4 5 6 7 8 UserControllerMockMvcTest 只测试 MVC 行为,不使用 PowerMock LegacySecurityAdapterTest 只测试静态遗留接口,使用 PowerMock UserServiceTest 只测试业务逻辑,使用 Mockito
测试类越聚焦,失败时越容易定位。
十六、推荐的测试编写结构 1. Arrange-Act-Assert 1 2 3 4 5 6 7 8 9 10 11 12 @Test public void shouldReturnUser () { when (repository.findById(1L )) .thenReturn(Optional.of(entity)); UserDto result = service.findById(1L ); assertEquals("mario" , result.getUsername()); }
也可以采用 Given-When-Then 命名:
1 2 3 @Test public void givenExistingUser_whenFindById_thenReturnUser () { }
推荐在团队内统一风格,不必在方法名里举行英语语法考试。
2. 一个测试只验证一个业务场景 不推荐:
1 2 3 4 5 6 7 8 @Test public void testEverything () { }
推荐:
1 2 3 4 shouldCreateUser() shouldReturnUser() shouldRejectDuplicateUsername() shouldThrowExceptionWhenUserDoesNotExist()
3. 测试名称表达业务结果 不推荐:
1 2 3 @Test public void testCreate1 () { }
推荐:
1 2 3 @Test public void shouldRejectCreationWhenUsernameAlreadyExists () { }
十七、应该 Mock 什么,不应该 Mock 什么 适合 Mock 的对象
Repository;
RPC Client;
HTTP Client;
Redis Client;
消息生产者;
文件系统网关;
时间、随机数、ID 生成器;
第三方 SDK;
支付、短信、邮件服务。
通常不需要 Mock 的对象
普通 DTO;
Entity;
Value Object;
简单集合;
纯函数工具类;
没有外部依赖的简单领域对象。
不推荐:
1 2 UserDto user = mock(UserDto.class);when (user.getUsername()).thenReturn("mario" );
推荐:
1 UserDto user = new UserDto (1L , "mario" , "Super Mario" );
真实数据对象更直观,也不容易因为新增 Getter 而让测试行为变得神秘。
十八、MockMvc 测试应该验证哪些内容 一个合理的 Controller 测试通常验证以下内容:
1. 状态码 1 .andExpect(status().isOk())
2. Content-Type 1 2 3 .andExpect(content().contentTypeCompatibleWith( MediaType.APPLICATION_JSON ))
3. 响应结构 1 2 .andExpect(jsonPath("$.success" ).value(true )) .andExpect(jsonPath("$.data.id" ).value(1 ))
4. 错误映射 1 2 .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message" ).value("User not found: 99" ))
5. Service 调用参数 1 verify(userService).findById(1L );
不要在 Controller 测试中重复验证 Service 内部细节 Controller 测试不应该关心:
Repository 调用了几次;
SQL 怎么执行;
Service 内部用了哪个 Mapper;
DTO 是通过 MapStruct 还是手写转换。
这些应由 Service 或 Repository 测试负责。
十九、常见异常与排查方式 1. ClassNotPreparedException 异常示例:
1 The class LegacySecurityContext not prepared for test
原因:缺少 @PrepareForTest。
解决:
1 @PrepareForTest(LegacySecurityContext.class)
构造器 Mock 时,准备调用 new 的类:
1 @PrepareForTest(LegacyReportService.class)
2. NoSuchMethodError 异常示例:
1 java.lang.NoSuchMethodError: org.mockito...
常见原因:
Mockito 版本被 Spring Boot BOM 升级;
PowerMock 依赖了旧版 Mockito 内部 API;
项目同时存在多个 Mockito 版本;
mockito-core 与 mockito-inline 冲突。
排查:
1 mvn dependency:tree -Dincludes=org.mockito
处理方式:
显式固定 Mockito 版本;
排除传递依赖;
不要在 PowerMock 模块中随意使用 Mockito 5;
最终方案是移除 PowerMock。
3. InaccessibleObjectException 在 JDK 17、21 等版本中,PowerMock 可能因为 Java 模块系统限制而失败:
1 java.lang.reflect.InaccessibleObjectException
临时方案可能需要在 Surefire 中增加:
1 2 3 4 5 6 7 8 <configuration > <argLine > --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED </argLine > </configuration >
但这只是兼容性补丁,不是长期设计方案。每增加一个 --add-opens,都像在技术债墙上又凿了一个通风孔:暂时能呼吸,但房子并没有变新。
4. Wanted but not invoked 异常示例:
1 2 Wanted but not invoked: userService.findById(1L)
检查:
被测试对象是否使用了正确的 Mock;
是否手动 new 了另一个 Service;
参数是否完全一致;
代码分支是否真的执行;
PowerMock 类加载器是否创建了不同类型实例;
是否在验证前发生异常。
5. InvalidUseOfMatchersException 错误:
1 2 when (repository.query("ORDER" , anyInt())) .thenReturn(result);
正确:
1 2 when (repository.query(eq("ORDER" ), anyInt())) .thenReturn(result);
同一次方法调用中,原始值和 Matcher 不要混用。
6. MockMvc 返回 404 常见原因:
URL 写错;
@RequestMapping 前缀遗漏;
Controller 没有注册到 MockMvc;
HTTP 方法不匹配;
PathVariable 模板不正确。
可以临时打印请求结果:
1 2 mockMvc.perform(get("/api/users/1" )) .andDo(print());
需要静态导入:
1 import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
7. MockMvc 返回 415 1 415 Unsupported Media Type
通常是 POST/PUT 请求未设置 Content-Type:
1 .contentType(MediaType.APPLICATION_JSON)
并确保请求体是合法 JSON:
1 .content(objectMapper.writeValueAsString(request))
8. MockMvc 返回 400 可能原因:
JSON 字段类型错误;
RequestBody 缺失;
参数校验失败;
日期格式不匹配;
枚举值非法;
缺少无参构造器或 Jackson 无法反序列化。
建议先增加:
然后查看 Resolved Exception。
二十、PowerMock 为什么不适合作为新项目默认方案 1. 依赖 Mockito 内部实现 PowerMock 不是只调用 Mockito 的稳定公共 API,它与 Mockito 内部机制耦合,因此 Mockito 升级后容易直接崩溃。
2. 自定义类加载器增加复杂度 它可能与以下技术冲突:
Spring CGLIB 代理;
JDK 动态代理;
JaCoCo;
Mockito Inline;
JDK 模块系统;
XML/SSL/JMX 类;
应用服务器类加载器。
3. 它会掩盖设计问题 如果大量代码都需要 Mock:
static 方法;
private 方法;
new 构造器;
final 类;
静态初始化块;
通常说明依赖没有被显式建模。
4. 测试对实现细节高度敏感 例如构造器从:
1 new LegacyPdfClient (endpoint)
改成:
1 new LegacyPdfClient (endpoint, timeout)
whenNew 测试就会立即失败,即使业务行为没有变化。
二十一、现代 Mockito 对 PowerMock 的替代 Mockito 3.4 之后已经支持静态方法 Mock;Mockito 5 默认使用 Inline Mock Maker,并要求至少 Java 11。
1. Mockito MockedStatic 1 2 3 4 5 6 7 8 9 10 11 try (MockedStatic<LegacySecurityContext> mockedStatic = Mockito.mockStatic(LegacySecurityContext.class)) { mockedStatic.when (LegacySecurityContext::currentOperator) .thenReturn("admin" ); String operator = LegacySecurityContext.currentOperator(); assertEquals("admin" , operator); mockedStatic.verify(LegacySecurityContext::currentOperator); }
静态 Mock 必须在作用域结束后关闭,因此推荐使用 try-with-resources。
2. Mockito MockedConstruction 1 2 3 4 5 6 7 8 9 10 11 12 13 14 try (MockedConstruction<LegacyPdfClient> mocked = Mockito.mockConstruction( LegacyPdfClient.class, (mock, context) -> { when (mock.generate("USER-1" )) .thenReturn(new byte []{1 , 2 , 3 }); } )) { byte [] result = new LegacyReportService () .generateUserReport(1L ); assertArrayEquals(new byte []{1 , 2 , 3 }, result); }
现代 Mockito 能替代 PowerMock 的一部分能力,但从长期维护看,重构依赖关系仍然优于字节码 Mock。
二十二、比 Mock 静态方法更好的做法:适配器封装 原代码:
1 String operator = LegacySecurityContext.currentOperator();
可以封装成接口:
1 2 3 4 public interface OperatorProvider { String currentOperator () ; }
生产实现:
1 2 3 4 5 6 7 public class LegacyOperatorProvider implements OperatorProvider { @Override public String currentOperator () { return LegacySecurityContext.currentOperator(); } }
Controller 改成构造器注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class UserController { private final UserService userService; private final OperatorProvider operatorProvider; public UserController ( UserService userService, OperatorProvider operatorProvider) { this .userService = userService; this .operatorProvider = operatorProvider; } }
测试时只使用普通 Mockito:
1 2 3 4 5 @Mock private OperatorProvider operatorProvider;when (operatorProvider.currentOperator()) .thenReturn("admin" );
这种方式的优点:
不依赖 PowerMock;
不修改字节码;
可迁移到 JUnit 5/6;
对 JDK 版本更友好;
业务类依赖更明确;
更容易替换第三方 SDK。
二十三、比 Mock new 更好的做法:工厂或依赖注入 原代码:
1 LegacyPdfClient client = new LegacyPdfClient (endpoint);
重构为工厂:
1 2 3 4 public interface PdfClientFactory { LegacyPdfClient create (String endpoint) ; }
生产实现:
1 2 3 4 5 6 7 public class DefaultPdfClientFactory implements PdfClientFactory { @Override public LegacyPdfClient create (String endpoint) { return new LegacyPdfClient (endpoint); } }
业务服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ReportService { private final PdfClientFactory clientFactory; private final String endpoint; public ReportService ( PdfClientFactory clientFactory, String endpoint) { this .clientFactory = clientFactory; this .endpoint = endpoint; } public byte [] generateUserReport(Long userId) { LegacyPdfClient client = clientFactory.create(endpoint); return client.generate("USER-" + userId); } }
测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Mock private PdfClientFactory clientFactory;@Mock private LegacyPdfClient pdfClient;@Test public void shouldGenerateReport () { when (clientFactory.create(endpoint)).thenReturn(pdfClient); when (pdfClient.generate("USER-1" )) .thenReturn(new byte []{1 , 2 , 3 }); byte [] result = reportService.generateUserReport(1L ); assertArrayEquals(new byte []{1 , 2 , 3 }, result); verify(clientFactory).create(endpoint); verify(pdfClient).generate("USER-1" ); }
二十四、时间、随机数和 ID 生成器不要写死 下面这些静态调用会让测试变得困难:
1 2 3 4 LocalDateTime.now(); UUID.randomUUID(); System.currentTimeMillis(); Snowflake.nextId();
可以显式注入:
1 2 3 4 5 6 7 8 9 10 public class OrderService { private final Clock clock; private final IdGenerator idGenerator; public OrderService (Clock clock, IdGenerator idGenerator) { this .clock = clock; this .idGenerator = idGenerator; } }
测试时间:
1 2 3 4 Clock fixedClock = Clock.fixed( Instant.parse("2026-07-28T00:00:00Z" ), ZoneOffset.UTC );
这比 Mock LocalDateTime.now() 更简单,也更稳定。
二十五、测试覆盖率不是最终目标 覆盖率只能说明代码是否被执行,不能证明测试是否有效。
以下测试覆盖率可能很高,但价值很低:
1 2 3 4 @Test public void shouldRun () { service.process(request); }
它没有验证:
返回值;
状态变化;
调用行为;
异常分支;
边界条件;
幂等性。
更有价值的测试应覆盖:
正常路径;
关键异常路径;
边界值;
重复提交;
空值和非法值;
外部依赖超时或失败;
状态迁移;
金额和数量精度;
权限与审计逻辑。
二十六、测试代码的工程化建议 1. 建立测试数据工厂 不要在每个测试里重复构造几十个字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 public final class UserTestData { private UserTestData () { } public static UserEntity defaultUser () { return new UserEntity ( 1L , "mario" , "Super Mario" ); } }
2. 避免共享可变测试数据 不推荐:
1 private static final UserEntity USER = new UserEntity (...);
如果某个测试修改它,后续测试可能被污染。
推荐每次创建新对象:
1 UserEntity user = UserTestData.defaultUser();
3. 测试之间不能依赖执行顺序 测试必须能够:
单独运行;
任意顺序运行;
并行运行时尽量安全;
重复执行得到一致结果。
4. 不要 Mock 你不拥有的复杂第三方类型 对于复杂第三方 SDK,可以封装 Adapter:
1 2 3 4 BusinessService -> PaymentGateway 接口 -> ThirdPartyPaymentAdapter -> 第三方 SDK
业务测试 Mock PaymentGateway,第三方适配器单独做契约测试或集成测试。
5. 测试失败信息要能直接定位问题 断言应表达业务语义:
1 2 3 4 5 assertEquals( "duplicate username should be rejected" , "Username already exists: mario" , exception.getMessage() );
复杂对象可使用 AssertJ,但不要为了链式语法把简单断言写成“测试书法展”。
二十七、推荐的测试拆分方案 graph LR
A[Controller] --> B[ControllerMockMvcTest]
C[Application Service] --> D[ServiceMockitoTest]
E[Repository] --> F[RepositoryIntegrationTest]
G[Legacy Static API] --> H[PowerMockTest]
I[Third-party Adapter] --> J[Contract/Integration Test]
推荐的测试类职责:
测试类
负责验证
不负责验证
ControllerMockMvcTest
HTTP、路由、JSON、异常映射
SQL、事务、业务细节
ServiceMockitoTest
业务分支、协作行为
Spring MVC、真实数据库
RepositoryIntegrationTest
SQL、映射、索引条件、事务
Controller
PowerMockTest
遗留静态或构造器边界
大量普通业务逻辑
EndToEndTest
核心业务全链路
所有边界组合穷举
二十八、从 PowerMock 迁移的实际路线 不要试图一次性删除整个项目中的 PowerMock。更可行的方式是逐步迁移。
阶段一:建立清单 统计:
1 2 3 4 5 grep -R "PowerMockito" src/test grep -R "PrepareForTest" src/test grep -R "PowerMockRunner" src/test
按使用原因分类:
静态方法;
构造器;
私有方法;
final 类;
静态初始化;
系统类。
阶段二:优先处理低成本场景 优先替换:
final 类 Mock:升级 Mockito;
静态工具类:Mockito mockStatic;
构造器:Mockito mockConstruction;
时间:注入 Clock;
UUID/雪花 ID:注入 IdGenerator。
阶段三:提取适配器 对静态第三方 SDK 建立 Adapter:
1 2 3 4 5 6 7 StaticVendorSdk ↓ VendorGatewayAdapter ↓ VendorGateway interface ↓ BusinessService
阶段四:迁移到现代 JUnit 把普通测试迁移到 JUnit Jupiter:
1 2 3 @ExtendWith(MockitoExtension.class) class UserServiceTest { }
PowerMock 测试暂时保留在 JUnit 4 模块或通过 Vintage 执行,直到全部消除。
阶段五:删除 PowerMock 依赖 最后再删除:
1 2 org.powermock:powermock-module-junit4 org.powermock:powermock-api-mockito2
并重新检查:
Maven 依赖树;
CI 测试;
JaCoCo 覆盖率;
JDK 升级参数;
Surefire 配置。
二十九、什么时候仍然可以使用 PowerMock PowerMock 仍可能适用于以下场景:
大型遗留系统;
代码冻结期;
第三方静态 SDK 无法替换;
缺少源码,无法重构;
需要先建立回归保护网,再逐步改造;
项目仍固定在 JDK 8/11、JUnit 4 和旧版 Mockito。
使用前应满足:
依赖版本固定;
PowerMock 测试数量受控;
不把 PowerMock 扩散到新代码;
每个 PowerMock 测试都对应一个明确的遗留原因;
技术债清单中记录后续重构方案。
三十、最终实践建议 一套可维护的 Java 测试体系,可以采用下面的规则:
JUnit 负责测试生命周期和断言;
Mockito 负责隔离普通依赖;
MockMvc 负责验证 Spring MVC Web 层;
PowerMock 只作为遗留代码过渡工具;
业务逻辑优先放在 Service 和领域对象中测试;
Controller 测试只关注 HTTP 契约;
Repository 使用真实数据库或 Testcontainers 做集成测试;
静态依赖逐步改造成接口、适配器、工厂或可注入组件;
不要为了覆盖率写没有业务断言的测试;
测试代码也属于生产资产,需要重构、评审和持续维护。
可以把四个框架总结为一句话:
JUnit 负责把测试跑起来,Mockito 负责让依赖安静下来,MockMvc 负责把 HTTP 演出来,PowerMock 负责把遗留代码暂时按住。
长期来看,最好的测试工具不是功能最强的 Mock 框架,而是让代码本身拥有清晰依赖、明确边界和稳定行为的设计。
参考资料