You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mockito静态方法Mock复用问题:多测试用例运行结果异常

问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 16:14:57