PowerMock和JMockit的使用与对比

本文对比了PowerMock和JMockit在静态方法模拟上的性能和使用便捷度。PowerMock通过自定义ClassLoader加载测试类,而JMockit通过字节码处理生成替换类。JMockit在调用Mock后的方法速度更快,且使用更灵活。

PowerMock的使用

之所以提到PowerMock而不是Mock,是因为自己服务器端的配置数据获取的方法是静态方法,如果使用mock方式来模拟数据,只有PowerMock支持Mock静态方法,Mock不支持。

引入PowerMock

<dependency>
	<groupId>org.powermock</groupId>
	<artifactId>powermock-module-junit4</artifactId>
	<version>2.0.0</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.powermock</groupId>
	<artifactId>powermock-api-mockito2</artifactId>
	<version>2.0.0</version>
	<scope>test</scope>
</dependency>

编写测试用例

@RunWith(PowerMockRunner.class) //该注解表示当前测试类的运行模式为PowerMock,不加这个注解,PowerMock不生效
@PrepareForTest({ NumericService.class }) //该注解表示要在当前测试类中进行Mock的类
@PowerMockIgnore({ "javax.management.*", "javax.crypto.*", "sun.security.ssl.*", "sun.security.jca.*", "javax.net.*" })//本行注解要求PowerMock使用自定义ClassLoader加载时,忽略一部分底层包,否则会出现各种“class not cast to”之类的诸多报错
public class PowerMockTest {
	@Test
	public void test() throws Exception {
		long time = System.currentTimeMillis();
		TestUtility.init();
		System.out.println(System.currentTimeMillis() - time);
		try {
			NumericService.getNumericById(TabShop.class, "forTest");
			fail();
		} catch (Exception e) {
		}
		PowerMockito.mockStatic(NumericService.class);
		PowerMockito.when(NumericService.getNumericById(TabShop.class, "forTest")).thenReturn(null);
		System.out.println(NumericService.getNumericById(TabShop.class, "forTest"));
	}

}

运行代码查看运行效果,已经是否Mock成功。

17:11:54.117 [main] ERROR com.***.utli.TestUtility - NCC Server started…
17:11:54.129 [main] INFO com.***.gs.common.JobThreadPoolExecutor - init corePoolSize:10
64269
null
17:11:54.931 [Thread-4] ERROR gamebase.base.server.BaseServer - Execute ShutdownHook Start
17:11:54.932 [Thread-4] ERROR gamebase.base.server.BaseServer - Execute ShutdownHook End

可见PowerMock已经成功将 NumericService.getNumericById(TabShop.class, "forTest") Mock住,并将返回值设置成null。
但是,注意在null值前面的数值,表示TestUtility.init();运行的时间,高达64秒,太耗时了,这是因为在TestUtility.init();中初始化了很多东西,PowerMock下运行时导致性能严重下降,基于这个原因,后来又找到JMock来实现对数据获取的模拟。

JMockit的使用

引入JMockit

<!-- 先声明jmockit的依赖 -->
<dependency>
	<groupId>org.jmockit</groupId>
	<artifactId>jmockit</artifactId>
	<version>1.36</version>
	<scope>test</scope>
	</dependency>
<!-- 再声明junit的依赖 -->

编写测试用例

public class JMockTest {
	@Test
	public void testExpectations() {
		try {
			NumericService.getNumericById(TabShop.class, "2");
			fail();
		} catch (Exception e) {
		}
		new Expectations(NumericService.class) {
			{
				NumericService.getNumericById(TabShop.class, "2");
				result = null;
			}
		};
		System.out.println(NumericService.getNumericById(TabShop.class, "2"));
	}

	@Test
	public void testMockUp() {
		try {
			NumericService.getNumericById(TabShop.class, "1");
			fail();
		} catch (Exception e) {
		}
		new MockUp<NumericService>() {
			@Mock
			public <T> T getNumericById(Class<T> clz, String numericId) {
				return null;
			}
		};
		System.out.println(NumericService.getNumericById(TabShop.class, "1"));
	}
}

ExpectationsMockUp的测试用例都能跑通,在这里不在贴上结果。不过可以看到,JMockit的使用比起PowerMock更加轻量级,不需要对类进行整个注解,而是在单个测试用例中使用new的方式进行修改。

比较

比较项目PowerMockJMockit
使用便捷度需要对整个测试类做注解在单个测试用例中使用即可
调用Mock后的方法10000遍消耗的时间(毫秒)1278483
准确度目前所知,PowerMock后的静态方法所有返回值都是Mock的值支持指定参数的指定返回值,不符合参数值的调用不走Mock
生命周期从类的加载到结束从调用new到当前测试用例结束
机制自定义ClassLoader加载测试类,并处理类中所有的引用,包括底层甚至JDK部分代码通过字节码处理,生成替换类,不对其他引用出处理

通过以上比较,目前确认JMockit更适合自己服务器端的测试用例使用

<think> 我们正在寻找能够模拟静态方法且不影响代码覆盖率的PowerMock替代方案。根据引用[1],PowerMock通过自定义类加载器字节码操作来模拟静态方法,但这会干扰覆盖率工具(如JaCoCo)的字节码插桩,导致覆盖率下降。因此,我们需要寻找不依赖字节码操作的替代技术。 ### 替代方案分析 #### 1. **避免静态方法:依赖注入重构** - **核心思路**:重构代码,用依赖注入(如构造函数注入或Setter注入)替代静态方法调用。这样可以直接使用Mockito等标准框架模拟实例方法。 - **优势**: - 完全兼容JaCoCo,覆盖率不受影响。 - 符合SOLID原则,提升代码可测试性[^1]。 - **示例**: 重构前: ```java public class OrderService { public void createOrder() { // 直接调用静态工具类 String id = IdGenerator.generateId(); // 静态方法 } } ``` 重构后: ```java public class OrderService { private final IdGenerator idGenerator; // 通过接口注入 public OrderService(IdGenerator idGenerator) { this.idGenerator = idGenerator; } public void createOrder() { String id = idGenerator.generateId(); // 实例方法 } } ``` 测试代码: ```java @RunWith(MockitoJUnitRunner.class) public class OrderServiceTest { @Mock private IdGenerator mockIdGenerator; @InjectMocks private OrderService orderService; @Test public void testCreateOrder() { when(mockIdGenerator.generateId()).thenReturn("mock-id"); orderService.createOrder(); // 验证逻辑 } } ``` - **覆盖率影响**:无冲突,覆盖率100%可达成。 #### 2. **Mockito 3.4.0+ 模拟静态方法(有限支持)** - **核心能力**:Mockito从3.4.0版本开始支持静态方法模拟(通过`mockStatic`),但仅限于短作用域(如单个测试用例内)[^5]。 - **原理**:基于线程局部的模拟上下文,而非字节码修改,避免JaCoCo冲突。 - **限制**: - 不支持模拟构造函数、final类或私有方法。 - 静态模拟必须在`try-with-resources`块中定义,确保及时清理。 - **示例**: ```java @RunWith(MockitoJUnitRunner.class) public class StaticMockTest { @Test public void testStaticMock() { try (MockedStatic<IdGenerator> mocked = mockStatic(IdGenerator.class)) { mocked.when(IdGenerator::generateId).thenReturn("mock-id"); // 调用被测代码 String id = IdGenerator.generateId(); assertEquals("mock-id", id); } // 超出此作用域后,静态模拟自动关闭 } } ``` - **覆盖率影响**:JaCoCo完全兼容,不会导致覆盖率下降。 #### 3. **使用接口包装静态调用** - **核心思路**:将静态方法调用封装到接口的实现类中,测试时注入模拟实现。 - **优势**:无需修改原始静态类,仅包装调用点。 - **示例**: 定义接口: ```java public interface IdService { String generateId(); } ``` 实现类(适配静态方法): ```java public class StaticIdService implements IdService { @Override public String generateId() { return IdGenerator.generateId(); // 委托给静态方法 } } ``` 业务代码改用接口: ```java public class OrderService { private final IdService idService; // 依赖接口 public OrderService(IdService idService) { this.idService = idService; } public void createOrder() { String id = idService.generateId(); } } ``` 测试时模拟`IdService`接口: ```java @RunWith(MockitoJUnitRunner.class) public class OrderServiceTest { @Mock private IdService mockIdService; @InjectMocks private OrderService orderService; @Test public void testCreateOrder() { when(mockIdService.generateId()).thenReturn("mock-id"); orderService.createOrder(); } } ``` - **覆盖率影响**:JaCoCo可正常跟踪,无冲突。 #### 4. **TestableMock(无反射的Java模拟工具)** - **核心特性**:通过编译时代码生成实现模拟,完全避免运行时代理或字节码操作[^2]。 - **优势**: - 支持模拟静态方法、构造函数、私有方法等。 - JaCoCo兼容,不影响覆盖率统计。 - **使用步骤**: 1. 添加Maven依赖: ```xml <dependency> <groupId>com.alibaba.testable</groupId> <artifactId>testable-core</artifactId> <version>0.7.10</version> <scope>test</scope> </dependency> ``` 2. 在测试类中使用`@MockWith`注解: ```java @RunWith(TestableMockRunner.class) public class OrderServiceTest { public static class Mock { // 模拟IdGenerator的静态方法 @MockMethod public static String generateId(IdGenerator ignored) { return "mock-id"; } } @Test public void testCreateOrder() { OrderService service = new OrderService(); service.createOrder(); // 验证逻辑 } } ``` - **覆盖率影响**:由于不修改字节码,JaCoCo无冲突。 ### 方案对比推荐 | 方案 | 模拟能力 | 覆盖率兼容性 | 代码侵入性 | 推荐指数 | |-----------------------|----------------------|--------------|------------------|----------| | 依赖注入重构 | 仅实例方法 | ★★★★★ | 高(需修改代码) | ★★★★☆ | | Mockito静态模拟 | 静态方法(短作用域) | ★★★★★ | 低 | ★★★★☆ | | 接口包装静态调用 | 静态方法 | ★★★★★ | 中(添加接口) | ★★★☆☆ | | TestableMock | 静态/私有/构造函数 | ★★★★★ | 低(仅测试类) | ★★★★★ | **推荐顺序**: 1. **TestableMock**:功能全面且零冲突,适合复杂场景。 2. **Mockito静态模拟**:轻量级解决方案,适合临时静态方法模拟。 3. **依赖注入重构**:长期提升代码质量,但改造成本较高。 > 注意:所有方案均优于PowerMock,因为它们避免了字节码操作,确保覆盖率工具正常工作。 §§相关问题§§ 1. 如何在不使用PowerMock的情况下模拟私有方法? 2. TestableMockMockito在性能上有何差异? 3. 依赖注入重构的最佳实践有哪些? 4. 如何升级Mockito以支持静态方法模拟? [^1]: 依赖注入重构可彻底解决静态方法导致的测试问题。 [^2]: TestableMock通过编译时代码生成避免字节码冲突。 [^3]: Mockito 3.4.0+的静态模拟功能适用于简单场景。 </think>### 除 PowerMock 外可模拟静态方法且不影响代码覆盖率的替代方案 #### 1. **Mockito 3.4+ 内置静态方法模拟** - **核心机制**: Mockito 从 3.4 版本开始支持静态方法模拟,通过 `MockedStatic` 对象实现。它基于 **线程局部作用域** 而非字节码操作,不会干扰 JaCoCo 的插桩过程[^5]。 - **优势**: - 完全兼容 JaCoCo,覆盖率保持绿色 - 无需特殊运行器(仍可使用 `MockitoJUnitRunner`) - 作用域结束后自动释放模拟 - **示例代码**: ```java try (MockedStatic<StaticClass> mockedStatic = mockStatic(StaticClass.class)) { mockedStatic.when(StaticClass::staticMethod).thenReturn("mock-value"); // 调用被测代码 String result = Service.doSomething(); assertEquals("expected", result); } // 作用域结束,静态模拟自动清除 ``` #### 2. **TestableMock(编译时代码生成方案)** - **核心机制**: 通过编译时生成模拟类(类似 Lombok),**完全不修改运行时字节码**,彻底避开 JaCoCo 冲突[^2][^4]。 - **特性对比**: | 能力 | PowerMock | TestableMock | |---------------------|----------|-------------| | 静态方法模拟 | ✔️ | ✔️ | | 私有方法模拟 | ✔️ | ✔️ | | 字节码操作 | 是 | **否** | | 覆盖率兼容性 | ❌ | ✔️ | - **Maven 依赖**: ```xml <dependency> <groupId>com.alibaba.testable</groupId> <artifactId>testable-core</artifactId> <version>0.7.10</version> <scope>test</scope> </dependency> ``` - **使用示例**: ```java @RunWith(MockitoJUnitRunner.class) public class ServiceTest { @TestableMock private String mockStaticMethod() { return "mock-value"; // 自动替换静态方法 } } ``` #### 3. **依赖注入重构(根本解决方案)** - **核心思想**: 通过设计模式消除静态调用,将静态方法封装为 **实例方法** + **接口注入**[^1][^3]。 - **重构步骤**: 1. 创建接口(如 `IdGenerator`) 2. 实现类包装静态方法(`StaticIdGenerator`) 3. 通过构造函数/SETTER注入 - **重构示例**: ```java // 重构前 (直接调用静态方法) public class OrderService { public void createOrder() { String id = UUIDGenerator.generate(); // 静态方法 } } // 重构后 (依赖注入) public class OrderService { private final IdGenerator idGenerator; // 接口 public OrderService(IdGenerator idGenerator) { this.idGenerator = idGenerator; } public void createOrder() { String id = idGenerator.generate(); // 实例方法 } } // 测试时用Mockito模拟 @Mock IdGenerator mockGenerator; @InjectMocks OrderService service; when(mockGenerator.generate()).thenReturn("mock-id"); ``` #### 4. **JMockit(备选方案,需谨慎配置)** - **适用场景**: 需模拟 final 类等复杂场景时,通过 `-javaagent` 参数加载,**避免类加载冲突**。 - **优势**: 支持高级模拟能力,覆盖率兼容性优于 PowerMock - **配置关键**: 在 Maven Surefire 插件添加 JVM 参数: ```xml <argLine> -javaagent:${settings.localRepository}/org/jmockit/jmockit/1.49/jmockit-1.49.jar </argLine> ``` ### 各方案选择建议 | **场景** | **推荐方案** | **覆盖率影响** | |-----------------------------------|----------------------------|----------------| | 新项目或可重构代码 | 依赖注入 + Mockito | 零影响(绿色) | | 遗留系统静态方法模拟 | Mockito 3.4+ `MockedStatic`| 零影响(绿色) | | 需模拟私有方法/复杂静态逻辑 | TestableMock | 零影响(绿色) | | 必须模拟 final 类/构造函数 | JMockit(配置代理) | 低影响(黄色) | > **关键结论**:Mockito 3.4+ 的静态模拟 TestableMock 是**最优替代方案**,既满足功能需求,又保持 JaCoCo 覆盖率 100% 绿色[^4][^5]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值