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

Groovy中使用Mockito验证时触发UnfinishedStubbingException(内联模拟)

解决Groovy中Mockito模拟JavaFX FileChooser的UnfinishedStubbingException问题

我来帮你搞定这个困扰你的问题——在Groovy环境下用Mockito模拟JavaFX的final类FileChooser时,明明逻辑和纯Java项目完全一致,却抛出了UnfinishedStubbingException。先理清楚问题的来龙去脉,再给你几个可行的解决方案。

问题回顾

你用Mockito的孵化特性模拟javafx.stage.FileChooser(final类),核心代码大致如下:

FileChooser mockFC = mock(FileChooser.class)
doReturn(mockFC).when(spyCH).getFileChooser()
// ... 执行业务逻辑
verify(mockFC, times(1)).showOpenDialog(any())

结果执行到verify这一行时抛出了异常,提示信息还扯到了final方法(但showOpenDialog明明不是final)、缺失whenReturn这些无关内容。而且纯Java Gradle项目跑相同逻辑完全正常,只要一加Groovy插件和依赖,不管有没有Groovy文件都会出问题,换了ByteBuddy版本也没用。你的环境是Mockito 2.7.22、Java 1.8.0_121,Groovy版本从2.3.11到3.0.0-alpha-1都试过了。

为啥会出这问题?

主要是Groovy的动态特性和Mockito的字节码操作“打架”了:

  1. Groovy的全局方法拦截:Groovy会对所有方法调用做动态拦截,包括Mockito生成的代理类方法,这会打乱Mockito的stubbing和验证逻辑,让它误以为你有未完成的stub操作。
  2. Mockito 2.x和Groovy的兼容性bug:Mockito 2.7.x版本的final类模拟特性还在孵化阶段,依赖ByteBuddy修改字节码,而Groovy的编译/运行时字节码处理逻辑和ByteBuddy在Java 8环境下有冲突,导致验证步骤被误判。

可行的解决方案

方案1:升级Mockito到兼容版本(最推荐)

Mockito 2.7.x太老了,后续版本对Groovy的兼容性做了优化,而且把final类模拟的功能从孵化特性改成了更稳定的mockito-inline扩展。你可以在Gradle里这么配置:

testImplementation 'org.mockito:mockito-core:2.28.2'
testImplementation 'org.mockito:mockito-inline:2.28.2'

替换掉原来的Mockito依赖,这样不需要手动配置ByteBuddy,mockito-inline会自动处理final类的模拟,而且能避开和Groovy的字节码冲突问题。

方案2:用Groovy友好的Mockito API

如果没法升级Mockito版本,可以试试引入mockito-groovy依赖,用Groovy风格的写法来写测试,同时确保Mockito的初始化优先级高于Groovy的动态代理:
首先加依赖:

testImplementation 'org.mockito:mockito-groovy:2.7.22'

然后调整测试类的写法,比如用JUnit的Mockito runner:

@RunWith(MockitoJUnitRunner.class)
class YourTest extends GroovyTestCase {
    @Spy
    YourClass spyCH

    @Mock
    FileChooser mockFC

    void testFileChooserInvocation() {
        doReturn(mockFC).when(spyCH).getFileChooser()
        // ... 执行你的业务代码
        verify(mockFC, times(1)).showOpenDialog(any())
    }
}

这样能让Mockito的代理逻辑更顺畅地和Groovy兼容。

方案3:改用测试替身绕开Mockito

如果上面的方案都不行,那就干脆不用Mockito模拟FileChooser,自己写个测试替身类来验证调用次数:

class TestFileChooser extends FileChooser {
    int showOpenDialogCalls = 0

    @Override
    File showOpenDialog(Window owner) {
        showOpenDialogCalls++
        return new File("test-file.txt") // 或者返回null,看你测试需要
    }
}

然后在测试里用这个替身:

TestFileChooser testFC = new TestFileChooser()
doReturn(testFC).when(spyCH).getFileChooser()
// ... 执行业务逻辑
assert testFC.showOpenDialogCalls == 1

这种方式完全避开了Mockito和Groovy的冲突,简单直接。

方案4:调整Groovy的运行参数

你还可以在Gradle里加一些Groovy的编译/运行参数,减少它的动态拦截行为,降低和Mockito的冲突:

tasks.withType(GroovyCompile) {
    groovyOptions {
        forkOptions.jvmArgs = [
            '-Dgroovy.use.classvalue=true',
            '-Dgroovy.dynamic.method.invocation=false'
        ]
    }
}

这些参数会让Groovy减少对方法调用的动态处理,从而减少对Mockito代理的干扰。

验证建议

优先试方案1,升级Mockito版本是最彻底的解决办法,能一劳永逸解决这类兼容性问题。如果因为项目限制没法升级,再试试方案2或方案3。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:47:09