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

ParameterizedTest中Mockito spy报错:不允许对Mock创建Spy

解决Mockito偶发报错“Spy is not allowed on mock”的问题

问题场景

参数化测试执行过程中,偶数次迭代会触发如下报错:

java.lang.IllegalArgumentException: Please don't pass mock here. Spy is not allowed on mock.

    at org.mockito.Mockito.spy(Mockito.java:2246)
    at org.mockito.kotlin.SpyingKt.spy(Spying.kt:52)

对应的测试代码结构如下:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@WebAppConfiguration
class MyTest {
    @MockitoSpyBean
    protected lateinit var ldapConnectionPoolHolder: LdapConnectionPoolHolder
    
    private lateinit var connectionPoolSpy: LdapConnectionPool
    private lateinit var connectionSpy: LdapConnection
    
    @BeforeEach
    fun setup() {
        connectionPoolSpy = spy(ldapConnectionPoolHolder.getConnectionPool())
        doReturn(connectionPoolSpy).whenever(ldapConnectionPoolHolder).getConnectionPool()
        connectionSpy = spy(connectionPoolSpy.getConnection()) // 偶发报错行
        doReturn(connectionSpy).whenever(connectionPoolSpy).getConnection()
    }

    @ParameterizedTest("...")
    @MethodSource("withoutCookiesSuccessScenarios")
    fun myTest(...) {
        // 测试逻辑
    }
}

尝试在测试结束后执行Mockito.reset(connectionPoolSpy, connectionSpy),问题仍未解决。

原因分析

参数化测试会复用测试类实例,代码存在两个关键问题:

  1. 第一次setup时,通过doReturn(connectionPoolSpy).whenever(ldapConnectionPoolHolder).getConnectionPool()给ldapConnectionPoolHolder设置了存根行为,后续迭代没有清除这个行为;
  2. 第二次迭代执行setup时,ldapConnectionPoolHolder.getConnectionPool()返回的是上一次迭代创建的connectionPoolSpy(而非原始真实对象),此时对这个已有的spy对象再次调用spy(),会触发Mockito的限制——不允许在mock/spy对象上嵌套spy。

之前调用reset(connectionPoolSpy, connectionSpy)只重置了这两个spy的行为,但没有清除ldapConnectionPoolHolder上的存根,所以getConnectionPool()始终返回旧的spy。

解决方案

方案1:每次setup前重置LdapConnectionPoolHolder的存根

在setup方法开头,先重置ldapConnectionPoolHolder,清除之前的存根行为,确保每次都能获取到真实的连接池对象再进行spy:

@BeforeEach
fun setup() {
    // 重置holder,清除上一次的存根行为
    reset(ldapConnectionPoolHolder)
    
    // 获取真实连接池并spy
    val realPool = ldapConnectionPoolHolder.getConnectionPool()
    connectionPoolSpy = spy(realPool)
    doReturn(connectionPoolSpy).whenever(ldapConnectionPoolHolder).getConnectionPool()
    
    // 获取真实连接并spy
    val realConn = connectionPoolSpy.getConnection()
    connectionSpy = spy(realConn)
    doReturn(connectionSpy).whenever(connectionPoolSpy).getConnection()
}

方案2:改用MockBean直接模拟依赖(更可控)

如果不需要依赖真实的连接池逻辑,直接用@MockBean模拟LdapConnectionPoolHolder和相关对象,避免真实对象的干扰:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@WebAppConfiguration
class MyTest {
    @MockBean
    protected lateinit var ldapConnectionPoolHolder: LdapConnectionPoolHolder
    
    @Mock
    private lateinit var connectionPoolSpy: LdapConnectionPool
    
    @Mock
    private lateinit var connectionSpy: LdapConnection
    
    @BeforeEach
    fun setup() {
        doReturn(connectionPoolSpy).whenever(ldapConnectionPoolHolder).getConnectionPool()
        doReturn(connectionSpy).whenever(connectionPoolSpy).getConnection()
    }

    // 测试方法...
}

方案3:避免使用类成员变量存储spy对象

将connectionPoolSpy和connectionSpy改为setup方法内的局部变量(如果测试方法不需要直接调用它们),或者每次setup都重新赋值,确保不会复用旧的spy实例。

内容的提问来源于stack exchange,提问作者gstackoverflow

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 08:27:21