Groovy中使用Mockito验证时触发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的字节码操作“打架”了:
- Groovy的全局方法拦截:Groovy会对所有方法调用做动态拦截,包括Mockito生成的代理类方法,这会打乱Mockito的stubbing和验证逻辑,让它误以为你有未完成的stub操作。
- 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

