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

GroovyMock模拟final类方法在库代码调用返回null的解决方法

问题背景
  • 多模块Java项目设置独立测试模块存放全项目单元测试、集成测试,测试模块src/main/java目录为空,所有测试代码均存放在test目录下。
  • 测试异常现象:使用GroovyMock模拟final类MyClass1的getText()方法时,Groovy测试文件内部直接调用可正常返回预设模拟值;但该方法被其他提前编译完成的依赖模块中MyClass2类调用时,会返回null/空字符串默认值,无法命中预设模拟逻辑。
  • 初步怀疑原因:Groovy通过编译期改写实现方法拦截,依赖库中的MyClass1、MyClass2均为已编译Java字节码,无法被该拦截逻辑处理。
问题复现代码

业务模块(Module1)代码

public final class MyClass1 {
  private String text;

  public MyClass1() {
    text = "Hello there!";
  }

  public String getText() {
    return text;
  }
}

public class MyClass2 {
  private MyClass1 myObject1;

  public MyClass2(MyClass1 myObject1) {
    this.myObject1 = myObject1;
  }

  public String toLowerCase() {
    return myObject1.getText().toLowerCase();
  }
}

测试代码

def assertTextNotNull(MyClass1 myObject1) {
  assert myObject1.getText() != null;
}

def "successful test"() {
  given:
  MyClass1 myMock1 = GroovyMock(MyClass1);

  myMock1.getText() >> "ABC";

  assertTextNotNull(myMock1);
}

def "unsuccessful test"() {
  given:
  MyClass1 myMock1 = GroovyMock(MyClass1); // 因待模拟的实际类是final类型,必须使用GroovyMock
  MyClass2 myObject2 = new MyClass2(myMock1);

  myMock1.getText() >> "ABC";

  when:
  assertTextNotNull(myMock1);

  then:
  myObject2.toLowerCase() == "abc"; // 此处执行失败,原因是myObject1.getText()返回空字符串""
}

测试模块pom配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>myGroupId</groupId>
        <artifactId>myParentArtifactId</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>myArtifactId-tests</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <release>17</release>
                </configuration>
                <dependencies>

                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
                <configuration>
                    <excludes>
                        <exclude>**/*IT.*</exclude>
                        <exclude>**/*IntegrationTest.*</exclude>
                    </excludes>
                    <includes>
                        <include>**/*UT.*</include>
                        <include>**/*UnitTest.*</include>
                        <include>**/*UT.groovy</include>
                    </includes>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>3.0.0-M7</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                        <configuration>
                            <excludes>
                                <exclude>**/*UT.*</exclude>
                                <exclude>**/*UnitTest.*</exclude>
                            </excludes>
                            <includes>
                                <include>**/*IT.*</include>
                                <include>**/*IntegrationTest.*</include>
                            </includes>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <!-- gmavenplus插件用于编译Groovy代码 -->
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>1.13.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compileTests</goal>
                            <goal>addSources</goal>
                            <goal>addTestSources</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

    <repositories>
        <repository>
            <id>papermc</id>
            <url>https://repo.papermc.io/repository/maven-public/</url>
        </repository>
    </repositories>

    <dependencies>
        <!-- Paper/Waterfall依赖 -->
        <dependency>
            <groupId>io.papermc.paper</groupId>
            <artifactId>paper-api</artifactId>
            <version>1.18.2-R0.1-SNAPSHOT</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.github.waterfallmc</groupId>
            <artifactId>waterfall-api</artifactId>
            <version>1.18-R0.1-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>
        <!-- 其他业务模块依赖 -->
        <dependency>
            <groupId>myGroupId</groupId>
            <artifactId>myArtifactId-module1</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>test</scope>
        </dependency>
        <!-- 测试相关依赖 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
            <!-- 配合CGLIB实现无默认构造函数类的Mock -->
        <dependency>
            <groupId>org.objenesis</groupId>
            <artifactId>objenesis</artifactId>
            <version>3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-core</artifactId>
            <version>2.2-M1-groovy-4.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib-nodep</artifactId>
            <version>3.3.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>
排查结论
  • 复现验证:将示例代码复制到新模块,保留MyClass1的final修饰符可稳定复现异常;移除final修饰符后测试可正常通过。
  • 根因确认:GroovyMock对final类的方法模拟仅在Groovy代码调用链路中生效,当方法被已编译的第三方库/其他模块Java代码调用时,会直接返回对应类型默认值(引用类型返回null,示例中字符串返回空字符串)。
可行解决方案

方案1:替换Mock框架为支持字节码增强的Mock工具(推荐)

放弃GroovyMock,改用支持类加载期字节码插桩的Mock框架,可无视final修饰符实现全链路Mock:

  • 优先选Mockito-inline(高版本Mockito已将该能力合并入核心包):原生支持Mock final类、静态方法,对Java/Groovy调用链路完全生效,和Spock 2.x版本兼容度很高。当前pom里已经引入了objenesis、cglib依赖,替换时只需要添加mockito-inline依赖,把测试代码里的GroovyMock(MyClass1)替换为Mock(MyClass1)即可,原有桩逻辑不需要改动。
  • 如果需要Mock私有方法、构造函数等更特殊的场景,可以用PowerMock配合Mockito使用,但配置复杂度更高,JDK17下需要额外添加JVM启动参数打开模块访问权限。

方案2:测试阶段剥离final修饰符

使用字节码处理工具在测试执行前修改依赖Jar包中目标类的字节码,移除final修饰符:

  • 可通过maven插件在测试编译阶段调用ByteBuddy等字节码工具,修改对应类的访问标志,去掉ACC_FINAL标记,让CGLIB可以正常生成动态代理类,此时Spock普通的Mock()方法即可正常工作。
  • 该方案不需要调整测试代码逻辑,但需要额外配置构建插件,对构建流程有侵入,适合不想更换现有Mock技术栈的场景。

方案3:针对目标类编写测试专用包装类

如果被Mock的final类方法数量不多,可在测试模块内定义对应接口和包装类,业务代码依赖接口而非具体final类,测试时直接Mock接口即可。

  • 该方案符合依赖倒置设计原则,但需要调整业务代码的依赖关系,改造成本较高,适合长期迭代的项目使用。

注意:JDK17环境下使用字节码增强类Mock框架时,需要在surefire/failsafe插件配置中添加JVM参数,打开对应JDK模块的访问权限,否则会报字节码操作失败的错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 02:15:39