使用Mockito和JUnit模拟静态方法时出现StackOverflowError
解决Mockito模拟Math静态方法导致StackOverflowError的问题
我在练习用JUnit和Mockito测试静态方法时遇到了StackOverflowError,场景是测试一个调用Math.pow的计算器方法。
实现类代码
class LessSimpleCalculatorImp implements LessSimpleCalculator { @Override public int power(int x, int y) { return (int)Math.pow(x, y); // 想验证Math.pow是否被调用 } }
测试用例代码
@RunWith(MockitoJUnitRunner.class) public class LessSimpleCalculator_Test { @Test public void test_pow() { try(MockedStatic<Math> mockedMath = Mockito.mockStatic(Math.class);) { mockedMath.when(() -> Math.pow(3, 2)).thenReturn(9.0); int result = lessSimpleCalculator.power(3, 2); assertEquals(9, result); mockedMath.verify(() -> Math.pow(3, 2), times(1)); } } }
执行mvn clean test时的错误
运行后抛出StackOverflowError,完整堆栈信息如下:
------------------------------------------------------- T E S T S ------------------------------------------------------- Forking command line: /bin/sh -c cd /home/username/Desktop/jarvis/core_java/jdbc && /usr/lib/jvm/java-18-openjdk-amd64/bin/java -jar /home/username/Desktop/jarvis/core_java/jdbc/target/surefire/surefirebooter12164676474631886113.jar /home/username/Desktop/jarvis/core_java/jdbc/target/surefire/surefire5274553330454463948tmp /home/username/Desktop/jarvis/core_java/jdbc/target/surefire/surefire_015841166298835331040tmp Running ca.jrvs.exercise.mockito.LessSimpleCalculator_Test OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at ./src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 873 *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at ./src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 873 Exception: java.lang.StackOverflowError thrown from the UncaughtExceptionHandler in thread "main" Results : Tests run: 0, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.105 s [INFO] Finished at: 2024-09-19T14:17:59-04:00 [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project jrvs-jdbc-exercise: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test failed: The forked VM terminated without saying properly goodbye. VM crash or System.exit called ? -> [Help 1] org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project jrvs-jdbc-exercise: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test failed: The forked VM terminated without saying properly goodbye. VM crash or System.exit called ? at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:215) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:156) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:148) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81) at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56) at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192) at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105) at org.apache.maven.cli.MavenCli.execute (MavenCli.java:957) at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:289) at org.apache.maven.cli.MavenCli.main (MavenCli.java:193) at jdk.internal.reflect.DirectMethodHandleAccessor.invoke (DirectMethodHandleAccessor.java:104) at java.lang.reflect.Method.invoke (Method.java:577) at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282) at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225) at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406) at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347) Caused by: org.apache.maven.plugin.PluginExecutionException: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test failed: The forked VM terminated without saying properly goodbye. VM crash or System.exit called ? at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo (DefaultBuildPluginManager.java:148) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:210) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:156) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:148) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81) at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56) at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192) at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105) at org.apache.maven.cli.MavenCli.execute (MavenCli.java:957) at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:289) at org.apache.maven.cli.MavenCli.main (MavenCli.java:193) at jdk.internal.reflect.DirectMethodHandleAccessor.invoke (DirectMethodHandleAccessor.java:104) at java.lang.reflect.Method.invoke (Method.java:577) at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282) at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225) at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406) at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347) Caused by: java.lang.RuntimeException: The forked VM terminated without saying properly goodbye. VM crash or System.exit called ? at org.apache.maven.plugin.surefire.booterclient.output.ForkClient.close (ForkClient.java:257) at org.apache.maven.plugin.surefire.booterclient.ForkStarter.fork (ForkStarter.java:301) at org.apache.maven.plugin.surefire.booterclient.ForkStarter.run (ForkStarter.java:116) at org.apache.maven.plugin.surefire.AbstractSurefireMojo.executeProvider (AbstractSurefireMojo.java:740) at org.apache.maven.plugin.surefire.AbstractSurefireMojo.executeAllProviders (AbstractSurefireMojo.java:682) at org.apache.maven.plugin.surefire.AbstractSurefireMojo.executeAfterPreconditionsChecked (AbstractSurefireMojo.java:648) at org.apache.maven.plugin.surefire.AbstractSurefireMojo.execute (AbstractSurefireMojo.java:586) at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo (DefaultBuildPluginManager.java:137) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:210) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:156) at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:148) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117) at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81) at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56) at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305) at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192) at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105) at org.apache.maven.cli.MavenCli.execute (MavenCli.java:957) at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:289) at org.apache.maven.cli.MavenCli.main (MavenCli.java:193) at jdk.internal.reflect.DirectMethodHandleAccessor.invoke (DirectMethodHandleAccessor.java:104) at java.lang.reflect.Method.invoke (Method.java:577) at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:282) at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:225) at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:406) at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:347) [ERROR] [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginExecutionException
Maven依赖配置
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>5.13.0</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>
问题原因与修复方案
核心原因
Mockito模拟Math类时,内部实现触发了循环调用:拦截Math.pow时,某些逻辑又调用了真实的Math方法,导致无限递归引发StackOverflowError。同时存在JUnit版本与Mockito依赖不匹配的问题——使用JUnit 4的@RunWith(MockitoJUnitRunner.class),但依赖的是JUnit Jupiter的Mockito包。
修复步骤
匹配Mockito与JUnit的依赖版本
- 若继续使用JUnit 4,替换Mockito依赖为
mockito-core和mockito-junit4(mockito-junit-jupiter是JUnit 5专用):<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.13.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit4</artifactId> <version>5.13.0</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> - 或切换到JUnit 5,用
@ExtendWith(MockitoExtension.class)替代@RunWith,保留mockito-junit-jupiter依赖。
- 若继续使用JUnit 4,替换Mockito依赖为
通过包装类隔离JDK静态调用
直接模拟JDK核心类易引发类加载或递归问题,建议用包装类封装Math.pow:- 新增包装类:
class MathWrapper { public double pow(int x, int y) { return Math.pow(x, y); } } - 修改计算器实现,通过构造函数注入包装类:
class LessSimpleCalculatorImp implements LessSimpleCalculator { private MathWrapper mathWrapper; public LessSimpleCalculatorImp(MathWrapper mathWrapper) { this.mathWrapper = mathWrapper; } @Override public int power(int x, int y) { return (int)mathWrapper.pow(x, y); } } - 测试时模拟
MathWrapper而非Math:@RunWith(MockitoJUnitRunner.class) public class LessSimpleCalculator_Test { @Mock private MathWrapper mathWrapper; @InjectMocks private LessSimpleCalculatorImp lessSimpleCalculator; @Test public void test_pow() { when(mathWrapper.pow(3, 2)).thenReturn(9.0); int result = lessSimpleCalculator.power(3, 2); assertEquals(9, result); verify(mathWrapper, times(1)).pow(3, 2); } }
- 新增包装类:
升级Maven Surefire插件
旧版本Surefire插件(2.12.4)与Java 18兼容性差,升级到最新稳定版(如3.2.5):<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.2.5</version> </plugin> </plugins> </build>
内容的提问来源于stack exchange,提问作者qwerty_99
相关产品推荐
相关产品推荐

