Java 单元测试Mockito与PowerMock_powermock mockito
Java 单元测试:Mockito 与 PowerMock 实战指南
在 Java 单元测试中,当被测试类依赖外部资源(如数据库、网络接口)或其他复杂组件时,直接测试会面临“依赖难隔离、环境难搭建”的问题。Mockito 和 PowerMock 是解决这类问题的核心工具:
- Mockito:轻量级 Mock 框架,专注于模拟非静态、非私有的依赖对象(如接口、普通类),简化依赖隔离。
- PowerMock:基于 Mockito 扩展,支持模拟 静态方法、私有方法、构造函数 等 Mockito 难以处理的场景,适合遗留系统或设计不够灵活的代码测试。
一、Mockito 核心用法(基础篇)
1. 环境准备
需引入依赖(以 Maven 为例):
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>4.8.1</version> <scope>test</scope> </dependency>
2. 核心注解与概念
@Mock
:创建一个 Mock 对象(模拟依赖)。@InjectMocks
:将@Mock
标记的对象自动注入到被测试类中(依赖注入)。@ExtendWith(MockitoExtension.class)
:JUnit 5 扩展,启用 Mockito 注解支持。
3. 基础操作:模拟行为与验证
示例场景
被测试类 OrderService
依赖 UserDao
和 ProductDao
,需测试 calculateTotalPrice
方法(根据用户等级和商品价格计算订单总价)。
// 被依赖的 Dao 接口(模拟对象) public interface UserDao { // 根据用户 ID 获取用户等级(1-普通,2-会员) int getUserLevel(Long userId); } public interface ProductDao { // 根据商品 ID 获取价格 double getPrice(Long productId); } // 被测试类 public class OrderService { private final UserDao userDao; private final ProductDao productDao; // 构造函数注入依赖 public OrderService(UserDao userDao, ProductDao productDao) { this.userDao = userDao; this.productDao = productDao; } // 计算订单总价:会员享 9 折,普通用户无折扣 public double calculateTotalPrice(Long userId, List<Long> productIds) { int level = userDao.getUserLevel(userId); double total = 0.0; for (Long pid : productIds) { total += productDao.getPrice(pid); } return level == 2 ? total * 0.9 : total; // 会员折扣 } }
测试代码(Mockito 实现)
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; import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) // 启用 Mockito 注解 public class OrderServiceTest { // 模拟依赖:UserDao 和 ProductDao @Mock private UserDao userDao; @Mock private ProductDao productDao; // 被测试对象:自动注入上面的 Mock 对象 @InjectMocks private OrderService orderService; // 测试 1:普通用户(等级 1)无折扣 @Test void calculateTotalPrice_normalUser_noDiscount() { // 1. Arrange(准备):设置 Mock 对象的行为 Long userId = 1L; List<Long> productIds = Arrays.asList(100L, 200L); // 模拟 userDao 返回普通用户(等级 1) when(userDao.getUserLevel(userId)).thenReturn(1); // 模拟 productDao 返回商品价格(100 和 200) when(productDao.getPrice(100L)).thenReturn(100.0); when(productDao.getPrice(200L)).thenReturn(200.0); // 2. Act(执行):调用被测试方法 double total = orderService.calculateTotalPrice(userId, productIds); // 3. Assert(断言):验证结果 assertEquals(300.0, total); // 100+200=300,无折扣 } // 测试 2:会员用户(等级 2)享 9 折 @Test void calculateTotalPrice_vipUser_withDiscount() { // 1. Arrange Long userId = 2L; List<Long> productIds = Arrays.asList(100L); when(userDao.getUserLevel(userId)).thenReturn(2); // 会员 when(productDao.getPrice(100L)).thenReturn(100.0); // 2. Act double total = orderService.calculateTotalPrice(userId, productIds); // 3. Assert assertEquals(90.0, total); // 100 * 0.9 = 90 } }
4. Mockito 进阶操作
(1)验证方法调用次数
用 verify
验证 Mock 对象的方法是否被正确调用:
@Test void testVerifyInvocation() { // 执行测试 orderService.calculateTotalPrice(1L, Arrays.asList(100L, 200L)); // 验证 userDao.getUserLevel(1L) 被调用了 1 次 verify(userDao, times(1)).getUserLevel(1L); // 验证 productDao.getPrice 被调用了 2 次(因为有 2 个商品) verify(productDao, times(2)).getPrice(anyLong()); // anyLong() 匹配任意 Long 参数 }
(2)模拟异常抛出
用 doThrow
模拟依赖方法抛出异常:
@Test void testExceptionHandling() { // 模拟:当 productId=999 时,productDao 抛出异常 when(productDao.getPrice(999L)).thenThrow(new RuntimeException(\"商品不存在\")); // 验证调用时是否抛出预期异常 assertThrows(RuntimeException.class, () -> { orderService.calculateTotalPrice(1L, Arrays.asList(999L)); }); }
(3)使用 ArgumentCaptor
捕获参数
当需要验证方法调用的参数详情时(如复杂对象),用 ArgumentCaptor
捕获参数:
import org.mockito.ArgumentCaptor; // ... @Test void testArgumentCaptor() { // 执行测试 orderService.calculateTotalPrice(1L, Arrays.asList(100L, 200L)); // 捕获 productDao.getPrice 的参数 ArgumentCaptor<Long> productIdCaptor = ArgumentCaptor.forClass(Long.class); verify(productDao, times(2)).getPrice(productIdCaptor.capture()); // 验证捕获的参数是否符合预期 List<Long> capturedIds = productIdCaptor.getAllValues(); assertEquals(Arrays.asList(100L, 200L), capturedIds); }
二、PowerMock 核心用法(解决 Mockito 局限性)
Mockito 无法直接模拟 静态方法、私有方法、构造函数 等,而 PowerMock 扩展了这些能力。以下是典型场景及解决方案。
1. 环境准备
需额外引入 PowerMock 依赖(注意版本兼容性,此处适配 JUnit 5 和 Mockito 4.x):
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-core</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit5</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency>
2. 模拟静态方法
场景
被测试类依赖一个含静态方法的工具类 DateUtils
,需模拟其静态方法返回固定值。
// 含静态方法的工具类 public class DateUtils { // 静态方法:获取当前年份 public static int getCurrentYear() { return Calendar.getInstance().get(Calendar.YEAR); } } // 被测试类(依赖 DateUtils 静态方法) public class OrderService { // ... 其他代码省略 ... // 计算订单年份前缀(如 2024 -> \"ORD-2024-\") public String getOrderPrefix() { int year = DateUtils.getCurrentYear(); // 调用静态方法 return \"ORD-\" + year + \"-\"; } }
测试代码(PowerMock 模拟静态方法)
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit5.PowerMockExtension; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.when; // 启用 PowerMock 扩展,并指定需要处理的静态类(DateUtils) @ExtendWith(PowerMockExtension.class) @PrepareForTest({DateUtils.class}) // 关键:声明需要模拟静态方法的类 public class OrderServiceStaticTest { @Test void testGetOrderPrefix() { // 1. 模拟静态类 DateUtils mockStatic(DateUtils.class); // 初始化静态模拟 // 2. 设置静态方法返回固定值(当前年份固定为 2024) when(DateUtils.getCurrentYear()).thenReturn(2024); // 3. 执行测试 OrderService orderService = new OrderService(null, null); // 其他依赖为 null(此处不影响) String prefix = orderService.getOrderPrefix(); // 4. 断言 assertEquals(\"ORD-2024-\", prefix); } }
3. 模拟私有方法
场景
被测试类有一个私有方法 calculateTax
,需模拟其返回值(避免依赖复杂逻辑)。
public class OrderService { // ... 其他代码省略 ... // 计算税费(私有方法,逻辑复杂) private double calculateTax(double total) { // 实际逻辑可能依赖外部税率接口,测试时需模拟 return total * 0.1; // 假设税率 10% } // 计算最终支付金额(总价 + 税费) public double calculatePayAmount(Long userId, List<Long> productIds) { double total = calculateTotalPrice(userId, productIds); double tax = calculateTax(total); // 调用私有方法 return total + tax; } }
测试代码(PowerMock 模拟私有方法)
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit5.PowerMockExtension; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.powermock.api.mockito.PowerMockito.*; @ExtendWith(PowerMockExtension.class) @PrepareForTest(OrderService.class) // 声明需要处理私有方法的类 public class OrderServicePrivateTest { @Test void testCalculatePayAmount_withMockPrivateMethod() throws Exception { // 1. 创建被测试类的 spy 对象(保留真实对象,仅模拟部分方法) OrderService orderService = spy(new OrderService(mock(UserDao.class), mock(ProductDao.class))); // 2. 模拟私有方法 calculateTax:当输入 300 时,返回 30(而非真实的 300*0.1=30,此处仅演示) doReturn(30.0).when(orderService, \"calculateTax\", 300.0); // 3. 模拟其他依赖(calculateTotalPrice 返回 300) doReturn(300.0).when(orderService).calculateTotalPrice(anyLong(), anyList()); // 4. 执行测试 double payAmount = orderService.calculatePayAmount(1L, Arrays.asList(100L, 200L)); // 5. 断言:300(总价) + 30(模拟的税费) = 330 assertEquals(330.0, payAmount); } }
4. 模拟构造函数
场景
被测试类在方法中直接 new
了一个 LogUtils
对象(硬编码依赖),需模拟该对象的行为。
// 日志工具类(被 new 出来的依赖) public class LogUtils { public void log(String message) { // 实际会写入文件或发送到日志服务,测试时需模拟 System.out.println(\"Log: \" + message); } } public class OrderService { // ... 其他代码省略 ... // 创建订单并记录日志(直接 new LogUtils) public void createOrder(Long orderId) { // 业务逻辑... LogUtils logUtils = new LogUtils(); // 硬编码依赖,难以用 Mockito 模拟 logUtils.log(\"Order created: \" + orderId); // 调用日志方法 } }
测试代码(PowerMock 模拟构造函数)
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit5.PowerMockExtension; import static org.powermock.api.mockito.PowerMockito.*; @ExtendWith(PowerMockExtension.class) @PrepareForTest({OrderService.class, LogUtils.class}) // 声明相关类 public class OrderServiceConstructorTest { @Test void testCreateOrder_withMockConstructor() throws Exception { // 1. 模拟 LogUtils 的构造函数:当 new LogUtils() 时,返回 Mock 对象 LogUtils mockLogUtils = mock(LogUtils.class); whenNew(LogUtils.class).withNoArguments().thenReturn(mockLogUtils); // 2. 执行测试 OrderService orderService = new OrderService(mock(UserDao.class), mock(ProductDao.class)); orderService.createOrder(123L); // 3. 验证日志方法被正确调用 verify(mockLogUtils).log(\"Order created: 123\"); } }
三、Mockito 与 PowerMock 最佳实践
-
优先使用 Mockito:
Mockito 轻量、简洁,适合大多数场景(模拟接口、普通类)。PowerMock 因需要修改字节码,可能导致测试复杂且与部分框架冲突,仅在必要时使用。 -
避免过度模拟:
若频繁需要模拟静态方法、私有方法,可能意味着代码设计存在问题(如耦合过高)。优先通过“依赖注入”“接口抽象”重构代码,减少对 PowerMock 的依赖。 -
注意版本兼容性:
PowerMock 与 JUnit、Mockito 版本强相关(如 PowerMock 2.0.9 适配 Mockito 4.x 和 JUnit 5),需严格匹配版本(参考 PowerMock 官方文档)。 -
测试命名与可读性:
含 Mock 的测试用例命名应明确模拟对象和场景,例如testCalculatePayAmount_whenTaxIsMocked_returnsCorrectTotal
。
四、总结
- Mockito 是 Java 单元测试的“标配”工具,专注于模拟常规依赖(接口、普通类),通过
@Mock
@InjectMocks
简化依赖隔离,核心能力包括模拟行为、验证调用、捕获参数。 - PowerMock 作为补充,解决 Mockito 无法处理的场景(静态方法、私有方法、构造函数),但需谨慎使用(避免掩盖代码设计问题)。
掌握这两个工具,可有效隔离复杂依赖,聚焦被测试单元的逻辑验证,大幅提升单元测试的覆盖率和可靠性,是保障代码质量的核心手段。以下是Java中使用Mockito进行单元测试的实际场景案例,覆盖常见测试需求(模拟依赖、验证交互、处理返回值/异常、参数匹配等),并附详细说明。
准备工作
- 依赖引入(Maven):
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency></dependencies>
- 核心注解说明:
@Mock
:创建一个模拟对象(替代真实依赖,避免依赖真实资源如数据库、网络)。@InjectMocks
:将@Mock
标注的模拟对象自动注入到被测试类中(依赖注入)。@ExtendWith(MockitoExtension.class)
:JUnit 5扩展,用于初始化Mockito注解的对象。
案例1:基础模拟(Service依赖DAO)
场景:测试用户服务(UserService
),其依赖用户DAO(UserDao
)。需模拟UserDao
,避免真实数据库操作。
代码结构
// 实体类public class User { private Long id; private String name; // 构造器、getter、setter}// DAO接口(依赖)public interface UserDao { User findById(Long id); // 根据ID查询用户 void save(User user); // 保存用户}// 服务类(被测试)public class UserService { private final UserDao userDao; // 依赖注入的DAO public UserService(UserDao userDao) { // 构造器注入 this.userDao = userDao; } // 根据ID查询用户(调用DAO) public User getUserById(Long id) { if (id == null || id <= 0) { throw new IllegalArgumentException(\"ID非法\"); } return userDao.findById(id); } // 注册用户(调用DAO保存) public void register(User user) { if (user == null || user.getName() == null) { throw new IllegalArgumentException(\"用户信息不能为空\"); } userDao.save(user); }}
测试代码
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;import static org.junit.jupiter.api.Assertions.*;import static org.mockito.Mockito.*;@ExtendWith(MockitoExtension.class) // 初始化Mockito注解class UserServiceTest { @Mock // 模拟UserDao依赖 private UserDao userDao; @InjectMocks // 将userDao注入到userService中 private UserService userService; // 测试:查询存在的用户(DAO返回非空) @Test void getUserById_Exist() { // 1. 准备测试数据 Long userId = 1L; User mockUser = new User(userId, \"张三\"); // 2. 模拟依赖行为:当调用userDao.findById(1L)时,返回mockUser when(userDao.findById(userId)).thenReturn(mockUser); // 3. 调用被测试方法 User result = userService.getUserById(userId); // 4. 断言结果 assertNotNull(result); assertEquals(userId, result.getId()); assertEquals(\"张三\", result.getName()); // 5. 验证交互:确保userDao.findById(1L)被调用了1次 verify(userDao, times(1)).findById(userId); } // 测试:查询不存在的用户(DAO返回null) @Test void getUserById_NotExist() { // 1. 准备测试数据 Long userId = 999L; // 2. 模拟依赖行为:当调用userDao.findById(999L)时,返回null when(userDao.findById(userId)).thenReturn(null); // 3. 调用被测试方法 User result = userService.getUserById(userId); // 4. 断言结果为null assertNull(result); // 5. 验证交互 verify(userDao, times(1)).findById(userId); } // 测试:输入非法ID(触发参数校验异常) @Test void getUserById_InvalidId() { // 1. 准备非法ID(如-1) Long invalidId = -1L; // 2. 调用被测试方法,预期抛出IllegalArgumentException IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> userService.getUserById(invalidId) ); // 3. 断言异常信息 assertEquals(\"ID非法\", exception.getMessage()); // 4. 验证交互:确保DAO未被调用(因为参数校验失败,不会执行DAO查询) verify(userDao, never()).findById(any()); // any()表示任意参数 } // 测试:注册用户(验证DAO的save方法被正确调用) @Test void register_ValidUser() { // 1. 准备测试数据 User user = new User(null, \"李四\"); // 2. 调用被测试方法(无需模拟save返回值,因为save是void方法) userService.register(user); // 3. 验证交互:确保userDao.save(user)被调用了1次 verify(userDao, times(1)).save(user); } // 测试:注册空用户(触发参数校验异常) @Test void register_NullUser() { // 1. 调用被测试方法,预期抛出异常 IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, () -> userService.register(null) ); // 2. 断言异常信息 assertEquals(\"用户信息不能为空\", exception.getMessage()); // 3. 验证DAO的save方法未被调用 verify(userDao, never()).save(any()); }}
案例2:模拟异常抛出
场景:测试依赖方法抛出异常时,被测试类的处理逻辑。例如:DAO查询时抛出SQLException
,服务层是否正确捕获并转换为自定义异常。
代码结构
// 自定义异常public class DataAccessException extends RuntimeException { public DataAccessException(String message) { super(message); }}// DAO接口(新增可能抛异常的方法)public interface UserDao { // ... 原有方法 User findByUsername(String username) throws SQLException; // 可能抛SQLException}// 服务类(处理异常)public class UserService { // ... 原有代码 public User getUserByUsername(String username) { try { return userDao.findByUsername(username); } catch (SQLException e) { // 将SQLException转换为自定义异常 throw new DataAccessException(\"查询用户失败:\" + e.getMessage()); } }}
测试代码
@Testvoid getUserByUsername_DaoThrowException() throws SQLException { // 1. 准备测试数据 String username = \"test\"; SQLException mockSqlEx = new SQLException(\"数据库连接超时\"); // 2. 模拟依赖行为:当调用findByUsername(\"test\")时,抛出mockSqlEx when(userDao.findByUsername(username)).thenThrow(mockSqlEx); // 3. 调用被测试方法,预期抛出自定义异常 DataAccessException exception = assertThrows( DataAccessException.class, () -> userService.getUserByUsername(username) ); // 4. 断言异常信息 assertTrue(exception.getMessage().contains(\"查询用户失败:数据库连接超时\")); // 5. 验证交互 verify(userDao, times(1)).findByUsername(username);}
案例3:参数匹配器(复杂参数场景)
场景:当方法参数复杂(如对象、集合),无法直接用具体值匹配时,使用Mockito的参数匹配器(如any()
、eq()
、argThat()
)。
代码结构
// 订单服务(被测试)public class OrderService { private final InventoryService inventoryService; // 依赖库存服务 public OrderService(InventoryService inventoryService) { this.inventoryService = inventoryService; } // 创建订单前检查库存 public boolean createOrder(Long productId, int quantity) { // 调用库存服务检查是否有足够库存 return inventoryService.checkStock(productId, quantity); }}// 库存服务(依赖)public interface InventoryService { boolean checkStock(Long productId, int quantity); // 检查库存}
测试代码
@ExtendWith(MockitoExtension.class)class OrderServiceTest { @Mock private InventoryService inventoryService; @InjectMocks private OrderService orderService; @Test void createOrder_WithParamMatchers() { // 1. 准备测试数据 Long productId = 100L; int quantity = 5; // 2. 模拟依赖行为:使用参数匹配器 // 当productId为任意Long类型,且quantity大于0时,返回true when(inventoryService.checkStock(any(Long.class), argThat(q -> q > 0))) .thenReturn(true); // 3. 调用被测试方法 boolean result = orderService.createOrder(productId, quantity); // 4. 断言结果 assertTrue(result); // 5. 验证交互(使用参数匹配器) verify(inventoryService, times(1)) .checkStock(eq(productId), eq(quantity)); // eq()用于精确匹配 }}
常用参数匹配器:
any(Type.class)
:匹配任意该类型的参数(如any(Long.class)
)。eq(value)
:匹配等于value
的参数(如eq(100L)
)。argThat(predicate)
:自定义匹配器(如argThat(q -> q > 0)
匹配正整数)。
案例4:Spy(部分模拟真实对象)
场景:需要保留对象的真实行为,仅模拟部分方法时,使用Spy
(间谍)。与@Mock
(完全模拟)不同,Spy
会调用对象的真实方法,除非被显式模拟。
代码结构
// 工具类(部分方法需要真实调用,部分需要模拟)public class StringUtils { // 真实方法:反转字符串 public String reverse(String str) { if (str == null) return null; return new StringBuilder(str).reverse().toString(); } // 需要模拟的方法:调用外部服务(测试时不希望真实调用) public boolean isExternalValid(String str) { // 真实逻辑:调用外部API,测试时需模拟 return true; }}// 依赖工具类的服务public class StringProcessor { private final StringUtils stringUtils; public StringProcessor(StringUtils stringUtils) { this.stringUtils = stringUtils; } public String process(String str) { if (!stringUtils.isExternalValid(str)) { // 依赖isExternalValid return \"无效字符串\"; } return stringUtils.reverse(str); // 依赖reverse }}
测试代码
@Testvoid process_WithSpy() { // 1. 创建真实对象的Spy(保留reverse的真实逻辑,模拟isExternalValid) StringUtils stringUtilsSpy = spy(new StringUtils()); // 2. 模拟Spy的部分方法:当调用isExternalValid(\"test\")时,返回true when(stringUtilsSpy.isExternalValid(\"test\")).thenReturn(true); // 3. 初始化被测试对象 StringProcessor processor = new StringProcessor(stringUtilsSpy); // 4. 调用被测试方法 String result = processor.process(\"test\"); // 5. 断言结果:reverse的真实逻辑被执行(\"test\"反转为\"tset\") assertEquals(\"tset\", result); // 6. 验证交互 verify(stringUtilsSpy, times(1)).isExternalValid(\"test\"); verify(stringUtilsSpy, times(1)).reverse(\"test\");}
核心总结
Mockito的核心价值是隔离被测试类与依赖,通过以下方式实现:
- 模拟依赖:用
@Mock
创建依赖的模拟对象,避免真实资源调用。 - Stub行为:用
when(xxx).thenReturn(yyy)
或thenThrow(zzz)
定义依赖的返回值/异常。 - 验证交互:用
verify(xxx, times(n)).method(...)
确保依赖被正确调用。 - 灵活匹配:用参数匹配器(
any()
、eq()
等)处理复杂参数场景。 - 部分真实:用
Spy
保留对象的真实行为,仅模拟需要的方法。
通过上述案例,可覆盖大部分日常单元测试场景,关键是明确“被测试类的逻辑”与“依赖的模拟边界”,确保测试聚焦于目标代码的逻辑正确性。