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),问题仍未解决。
原因分析
参数化测试会复用测试类实例,代码存在两个关键问题:
- 第一次
setup时,通过doReturn(connectionPoolSpy).whenever(ldapConnectionPoolHolder).getConnectionPool()给ldapConnectionPoolHolder设置了存根行为,后续迭代没有清除这个行为; - 第二次迭代执行
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
相关产品推荐
相关产品推荐

