Mockito静态方法Mock复用问题:多测试用例运行结果异常
我用Java的Mockito编写包含静态方法Mock的多个测试用例,单独运行每个用例时均正常,但运行整个测试类时,第一个执行的测试用例(如shouldRequirePricingIfSucceeding)的Mock会影响其他测试用例(如shouldNotRequirePricingIfRequestFails始终不会失败)。已尝试在每个测试方法内单独进行Mock操作,还添加了Mockito.clearAllCaches()的前置处理,但都无法解决问题。当前sendPriceRequest方法始终返回true,Mock无法切换为抛出异常的逻辑。使用环境为Java 8、Mockito 4.11.0和JUnit 5。
相关代码
PricingService.java
public class PricingService { public static final int PRODUCT_CODE = 10; private final ZoneId zoneId = ZoneId.of("UTC"); private static final Logger LOGGER = LoggerFactory.getLogger(PricingService.class); private static final PricingOauthApi pricingOauthApi = PricingOauthApiFactory.create(); private static final PricingApi pricingApi = PricingApiFactory.create(); private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); public boolean requiresPricingBeforeSendPush(PushCSCTransaction pushCSCTransaction) { int transactionType = pushCSCTransaction.getCscTransaction().getTransactionType(); if (!pushCSCTransaction.getControlArea().getSource().equals("TEST") || transactionType != 30 && transactionType != 31) return false; boolean sendRequestSuccessfully = sendPriceRequest(pushCSCTransaction); return sendRequestSuccessfully && transactionType == 31; } private boolean sendPriceRequest(PushCSCTransaction pushCSCTransaction) { try { OauthRequest oauthRequest = new OauthRequest(Configuration.Pricing.getOauthScope(), Configuration.Pricing.getOauthClientId(), Configuration.Pricing.getOauthClientSecret()); Response<OauthResponse> oauthResponse = pricingOauthApi.oauth(oauthRequest, Configuration.Pricing.getOauthPathId()).execute(); if (!oauthResponse.isSuccessful() || oauthResponse.body() == null) throw new WebApplicationException(""); Response<Void> response = pricingApi.pricing(pushCSCTransaction, "Bearer " + oauthResponse.body().getAccessToken()).execute(); return response.isSuccessful(); } catch (Exception exception) { LOGGER.error("Error when trying to send transaction message", exception); return false; } } }
PricingServiceTest.java
@ExtendWith(MockitoExtension.class) public class PricingServiceTest { @Test public void shouldNotRequirePricingIfRequestFails() throws IOException { PricingApi pricingApi = mock(); PricingOauthApi pricingOauthApi = mock(); try (MockedStatic<PricingApiFactory> pricingApiFactory = Mockito.mockStatic(PricingApiFactory.class); MockedStatic<PricingOauthApiFactory> pricingOauthApiFactory = Mockito.mockStatic(PricingOauthApiFactory.class)) { pricingApiFactory.when(PricingApiFactory::create).thenReturn(pricingApi); pricingOauthApiFactory.when(PricingOauthApiFactory::create).thenReturn(pricingOauthApi); Call<OauthResponse> call = mock(Call.class); Mockito.when(pricingOauthApi.oauth(Mockito.any(), Mockito.any())).thenReturn(call); Mockito.when(call.execute()).thenThrow(new IOException("Test")); PushCSCTransaction transaction = createCsvTransaction("TEST", 31, PRODUCT_CODE, 1312, 311, 1); PricingService service = new PricingService(); boolean result = service.requiresPricingBeforeSendPush(transaction); assertFalse(result); Mockito.verify(pricingOauthApi).oauth(Mockito.any(), Mockito.any()); Mockito.verify(pricingApi, Mockito.never()).pricing(Mockito.any(), Mockito.any()); } } @Test public void shouldRequirePricingIfSucceeding() throws IOException { PricingApi pricingApi = mock(); PricingOauthApi pricingOauthApi = mock(); try (MockedStatic<PricingApiFactory> pricingApiFactory = Mockito.mockStatic(PricingApiFactory.class); MockedStatic<PricingOauthApiFactory> pricingOauthApiFactory = Mockito.mockStatic(PricingOauthApiFactory.class)) { pricingApiFactory.when(PricingApiFactory::create).thenReturn(pricingApi); Call<Void> call = mock(Call.class); Mockito.when(pricingApi.pricing(Mockito.any(), Mockito.any())).thenReturn(call); Mockito.when(call.execute()).thenReturn(Response.success(null)); pricingOauthApiFactory.when(PricingOauthApiFactory::create).thenReturn(pricingOauthApi); Call<OauthResponse> callOauth = mock(Call.class); Mockito.when(pricingOauthApi.oauth(Mockito.any(), Mockito.any())).thenReturn(callOauth); OauthResponse oauthResponse = new OauthResponse(); Mockito.when(callOauth.execute()).thenReturn(Response.success(oauthResponse)); PushCSCTransaction transaction = createCsvTransaction("TEST", 31, PRODUCT_CODE, 1312, 311, 1); PricingService service = new PricingService(); boolean result = service.requiresPricingBeforeSendPush(transaction); assertTrue(result); Mockito.verify(pricingOauthApi).oauth(Mockito.any(), Mockito.any()); Mockito.verify(pricingApi).pricing(Mockito.any(), Mockito.any()); } } }
问题根源
问题出在PricingService的静态成员变量初始化逻辑:pricingOauthApi和pricingApi是static final修饰的成员,会在类第一次加载时完成初始化。第一个测试用例运行时,Mock的工厂会生成对应的Mock实例并赋值给这两个静态变量;后续测试用例即使重新Mock工厂,由于类已经加载完成,静态变量不会重新初始化,依然复用第一次的Mock实例,导致后续测试的Mock逻辑完全不生效。
Mockito.clearAllCaches()只能清除Mockito内部的缓存,无法重置JVM已加载类的静态成员变量,所以解决不了这个问题。
修复方案
方案1:重构PricingService,改用实例依赖(推荐)
把静态的API实例改成非静态,通过构造方法注入,让每个测试用例都能独立控制依赖的Mock实例:
public class PricingService { public static final int PRODUCT_CODE = 10; private final ZoneId zoneId = ZoneId.of("UTC"); private static final Logger LOGGER = LoggerFactory.getLogger(PricingService.class); // 改为实例成员 private final PricingOauthApi pricingOauthApi; private final PricingApi pricingApi; private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); // 业务代码用的构造方法,依旧通过工厂获取实例 public PricingService() { this.pricingOauthApi = PricingOauthApiFactory.create(); this.pricingApi = PricingApiFactory.create(); } // 为测试提供的构造方法,直接传入Mock实例 public PricingService(PricingOauthApi pricingOauthApi, PricingApi pricingApi) { this.pricingOauthApi = pricingOauthApi; this.pricingApi = pricingApi; } // 原有业务方法保持不变... }
对应简化测试类,无需再Mock静态工厂,直接注入Mock实例:
@ExtendWith(MockitoExtension.class) public class PricingServiceTest { @Mock private PricingApi pricingApi; @Mock private PricingOauthApi pricingOauthApi; @Test public void shouldNotRequirePricingIfRequestFails() throws IOException { // 直接用测试构造方法创建Service,传入独立的Mock实例 PricingService service = new PricingService(pricingOauthApi, pricingApi); Call<OauthResponse> call = mock(Call.class); when(pricingOauthApi.oauth(any(), any())).thenReturn(call); when(call.execute()).thenThrow(new IOException("Test")); PushCSCTransaction transaction = createCsvTransaction("TEST", 31, PRODUCT_CODE, 1312, 311, 1); boolean result = service.requiresPricingBeforeSendPush(transaction); assertFalse(result); verify(pricingOauthApi).oauth(any(), any()); verify(pricingApi, never()).pricing(any(), any()); } @Test public void shouldRequirePricingIfSucceeding() throws IOException { PricingService service = new PricingService(pricingOauthApi, pricingApi); Call<Void> call = mock(Call.class); when(pricingApi.pricing(any(), any())).thenReturn(call); when(call.execute()).thenReturn(Response.success(null)); Call<OauthResponse> callOauth = mock(Call.class); when(pricingOauthApi.oauth(any(), any())).thenReturn(callOauth); OauthResponse oauthResponse = new OauthResponse(); when(callOauth.execute()).thenReturn(Response.success(oauthResponse)); PushCSCTransaction transaction = createCsvTransaction("TEST", 31, PRODUCT_CODE, 1312, 311, 1); boolean result = service.requiresPricingBeforeSendPush(transaction); assertTrue(result); verify(pricingOauthApi).oauth(any(), any()); verify(pricingApi).pricing(any(), any()); } }
方案2:强制重置静态成员(不推荐,hack式)
如果无法修改PricingService的结构,可以在每个测试方法前重置静态成员变量,但这种方式依赖反射,破坏封装性,仅作为临时方案:
@ExtendWith(MockitoExtension.class) public class PricingServiceTest { @BeforeEach void resetStaticMembers() throws NoSuchFieldException, IllegalAccessException { // 反射重置pricingOauthApi静态变量 Field oauthApiField = PricingService.class.getDeclaredField("pricingOauthApi"); oauthApiField.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(oauthApiField, oauthApiField.getModifiers() & ~Modifier.FINAL); oauthApiField.set(null, null); // 反射重置pricingApi静态变量 Field pricingApiField = PricingService.class.getDeclaredField("pricingApi"); pricingApiField.setAccessible(true); modifiersField.setInt(pricingApiField, pricingApiField.getModifiers() & ~Modifier.FINAL); pricingApiField.set(null, null); } // 原有测试方法保持不变... }
总结
优先选择方案1,通过依赖注入重构代码,既解决了测试污染问题,也提升了代码的可维护性和可测试性;方案2仅作为无法修改源码时的临时 workaround。
内容的提问来源于stack exchange,提问作者MrAndre

