为什么我的 Mockito 内联 JUnit 测试在通过 Ant 运行 时崩溃,并在 IntelliJ IDEA 中成功?

Why do my Mockito-inline JUnit tests crash when run through Ant, and succeed in IntelliJ IDEA?

由于各种原因,我已经开始使用 Mockito 在我的单元测试中模拟一些 classes,并且在我开发的过程中,我让一切正常工作。完成更改后,我推送到我们的构建服务器,发现通过 Ant 运行 时单元测试被破坏。我已经能够在一个简单的精简案例中重现这一点,在该案例中我使用 Mockito 来测试被测 class 依赖项的模拟静态方法。下面我粘贴了源文件、测试文件、Ant 脚本和 Ivy 依赖文件,描述了我正在使用的每个库的版本(此时所有这些都是最新的),然后是详细错误 I看到了。

但是如果我在一个目录中创建这几个文件(src/com/example 下的两个 classes 和 tests/com/example 下的单元测试并将其作为 IntelliJ IDEA 项目打开,使用所有相同的依赖项(包括 JUnit)和 运行 所有测试,它成功了。与标准模块模板的唯一变化是将 JDK 路径和 Java 级别指定为 1.8。那是部分原因让它如此令人费解。就其价值而言,Ant 的行为是相同的,无论是从 IntelliJ IDEA 的内置 Ant 集成中调用,还是单独在命令行上调用。

BusinessClass.java

package com.example;

public class BusinessClass {
    public String returnString() {
        StaticDependency.DoSomething();
        return "Answer";
    }
}

StaticDependency.java

package com.example;

public class StaticDependency {
    public static void DoSomething() {
        throw new RuntimeException("Real code causes side effects I'm isolating from");
    }
}

BusinessClassTest.java

package com.example;

import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mockStatic;

public class BusinessClassTest {
    @Test
    public void returnString() {
        final BusinessClass classUnderTest = new BusinessClass();

        try (final MockedStatic<StaticDependency> ignored = mockStatic(StaticDependency.class)) {
            assertEquals("Answer", classUnderTest.returnString());
        }
    }
}

ivy.xml

<ivy-module version="2.0">
  <info organisation="com.example" module="MockitoTest"/>
  <dependencies>
    <dependency org="junit" name="junit" rev="4.13.2"/>

    <!-- Mockito and dependencies -->
    <dependency org="org.mockito" name="mockito-inline" rev="4.2.0"/>
    <dependency org="net.bytebuddy" name="byte-buddy-dep" rev="1.12.6"/>
    <dependency org="org.objenesis" name="objenesis" rev="3.2"/>
  </dependencies>
</ivy-module>

build.xml

<project default="all" xmlns:ivy="antlib:org.apache.ivy.ant">

  <property name="lib" location="Lib"/>
  <property name="src" location="src"/>
  <property name="tests" location="tests"/>
  <property name="build-test" location="build-test"/>
  <property name="build" location="build"/>

  <path id="build.classpath">
    <fileset dir="${lib}">
      <include name="**/*.jar"/>
    </fileset>
  </path>

  <target name="all" depends="clean, resolve, compile, test"/>

  <target name="clean">
    <delete dir="${build}"/>
    <delete dir="${build-test}"/>
    <delete dir="${lib}"/>
  </target>

  <target name="resolve" description="Download dependencies">
    <ivy:resolve file="ivy.xml"/>
    <mkdir dir="${lib}"/>
    <ivy:retrieve pattern="${lib}/[artifact]-[type].[ext]"/>
  </target>

  <target name="compile" description="Compile the source code">
    <mkdir dir="${build}"/>
    <javac destdir="${build}">
      <src path="${src}"/>
      <classpath refid="build.classpath"/>
    </javac>

    <mkdir dir="${build-test}"/>
    <javac destdir="${build-test}">
      <src path="${src}"/>
      <src path="${tests}"/>
      <classpath refid="build.classpath"/>
    </javac>
  </target>

  <target name="test" depends="compile" description="Run all unit tests">
    <junit>
      <classpath>
        <path refid="build.classpath"/>
        <path path="${build-test}"/>
      </classpath>
      <formatter type="plain" usefile="false"/>

      <test name="com.example.BusinessClassTest" />
    </junit>
  </target>
</project>

JUnit 输出

Testsuite: com.example.BusinessClassTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.889 sec

Testcase: returnString took 0.779 sec
    Caused an ERROR
Could not modify all classes [class com.example.StaticDependency]
org.mockito.exceptions.base.MockitoException: Could not modify all classes [class com.example.StaticDependency]
    at com.example.BusinessClassTest.returnString(Unknown Source)
Caused by: java.lang.IllegalStateException: 
Byte Buddy could not instrument all classes within the mock's type hierarchy

This problem should never occur for javac-compiled classes. This problem has been observed for classes that are:
 - Compiled by older versions of scalac
 - Classes that are part of the Android distribution
    at org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.triggerRetransformation(InlineBytecodeGenerator.java:280)
    at org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.mockClassStatic(InlineBytecodeGenerator.java:221)
    at org.mockito.internal.creation.bytebuddy.TypeCachingBytecodeGenerator.mockClassStatic(TypeCachingBytecodeGenerator.java:63)
    at org.mockito.internal.creation.bytebuddy.InlineDelegateByteBuddyMockMaker.createStaticMock(InlineDelegateByteBuddyMockMaker.java:560)
    at org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker.createStaticMock(InlineByteBuddyMockMaker.java:83)
    at org.mockito.internal.util.MockUtil.createStaticMock(MockUtil.java:147)
    at org.mockito.internal.MockitoCore.mockStatic(MockitoCore.java:142)
    at org.mockito.Mockito.mockStatic(Mockito.java:2164)
    at org.mockito.Mockito.mockStatic(Mockito.java:2101)
Caused by: java.lang.AbstractMethodError: org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator$ParameterWritingVisitorWrapper.wrap(Lnet/bytebuddy/description/type/TypeDescription;Lorg/objectweb/asm/ClassVisitor;Lnet/bytebuddy/implementation/Implementation$Context;Lnet/bytebuddy/pool/TypePool;Lnet/bytebuddy/description/field/FieldList;Lnet/bytebuddy/description/method/MethodList;II)Lorg/objectweb/asm/ClassVisitor;
    at net.bytebuddy.asm.AsmVisitorWrapper$Compound.wrap(AsmVisitorWrapper.java:746)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining$WithFullProcessing$RedefinitionClassVisitor.visit(TypeWriter.java:4906)
    at org.objectweb.asm.ClassReader.accept(ClassReader.java:569)
    at org.objectweb.asm.ClassReader.accept(ClassReader.java:424)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining.create(TypeWriter.java:3951)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:2213)
    at net.bytebuddy.dynamic.scaffold.inline.RedefinitionDynamicTypeBuilder.make(RedefinitionDynamicTypeBuilder.java:224)
    at net.bytebuddy.dynamic.scaffold.inline.AbstractInliningDynamicTypeBuilder.make(AbstractInliningDynamicTypeBuilder.java:123)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:3661)
    at org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.transform(InlineBytecodeGenerator.java:394)
    at sun.instrument.TransformerManager.transform(TransformerManager.java:188)
    at sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:428)
    at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
    at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:144)
    at org.mockito.internal.creation.bytebuddy.InlineBytecodeGenerator.triggerRetransformation(InlineBytecodeGenerator.java:276)

事实证明我的问题是我的 ivy.xml 文件中的 byte-buddy-dep 依赖性适得其反。正如 @temp-droid pointed out in a GitHub issue 我在 Mockito 项目中提出的同样的问题,将其替换为 byte-buddybyte-buddy-agent 会起作用。

然后我意识到 Ivy 不要求我明确列出子依赖项(我不知道它知道如何解决它们),所以我能够更新到更简单的 ivy.xml,它在 Ant 和 IntelliJ 中同样有效:

<ivy-module version="2.0">
  <info organisation="com.example" module="MockitoTest"/>
  <dependencies>
    <dependency org="junit" name="junit" rev="4.13.2"/>
    <dependency org="org.mockito" name="mockito-inline" rev="4.2.0"/>
  </dependencies>
</ivy-module>