【问题标题】:Mockito mocks locally final class but fails in JenkinsMockito 在本地模拟最终课程,但在 Jenkins 中失败
【发布时间】:2019-11-22 10:24:38
【问题描述】:

我已经为静态方法编写了一些单元测试。静态方法只接受一个参数。参数的类型是最终类。代码方面:

public class Utility {

   public static Optional<String> getName(Customer customer) {
       // method's body.
   }
}

public final class Customer {
   // class definition
}

所以对于Utility 类,我创建了一个测试类UtilityTests,我在其中为此方法编写了测试,getName。单元测试框架是TestNG,使用的模拟库是Mockito。所以一个典型的测试有以下结构:

public class UtilityTests {

   @Test
   public void getNameTest() {
     // Arrange
     Customer customerMock = Mockito.mock(Customer.class);
     Mockito.when(...).thenReturn(...);

     // Act
     Optional<String> name = Utility.getName(customerMock);

     // Assert
     Assert.assertTrue(...);
   }
}

有什么问题?

虽然测试在 IntelliJ 中在本地成功运行,但在 Jenkins 上却失败了(当我将代码推送到远程分支时,会触发构建并在最后运行单元测试)。错误信息如下:

org.mockito.exceptions.base.MockitoException:无法模拟/监视类 com.packagename.Customer Mockito 不能模拟/间谍,因为: - 最后一课

我尝试了什么?

我搜索了一下,以找到解决方案,但我没有成功。我在这里指出,我不允许更改 Customerfinal 类的事实。除此之外,如果可能的话,我希望完全改变它的设计(例如,创建一个接口,它将包含我想要模拟的方法并声明 Customer 类实现了该接口,正如何塞在他的评论中正确指出的那样)。我尝试的是mockito-final 中提到的第二个选项。尽管这解决了问题,但它阻止了其他一些单元测试:(,无法以明显的方式修复。

问题

所以这是我的两个问题:

  1. 首先这怎么可能?测试不应该在本地和詹金斯都失败吗?
  2. 如何根据我上面提到的约束来解决这个问题?

提前感谢您的帮助。

【问题讨论】:

  • 我的猜测是enable final 配置在您的工作区中有效,但在Jenkins 上运行时无法找到此文件。检查 Jenkins 在哪里查找文件以及它是否确实存在。
  • 另一个线程解释了如何在 Mockito 2 中启用最终类模拟,方法是在资源目录下添加一个 mockito 配置文件:stackoverflow.com/questions/14292863/…
  • 在您处理的代码中,是否有可能从 Customer 类中提取一个接口,比如 ICustomer,并在 Utility 类中使​​用它?然后你可以模拟那个接口而不是具体的最终类
  • @JoseTepedino 这是一个有效的观点。它确实完全有意义,并且绝对是克服这个问题的一种优雅方法。但是我想知道是否有另一种方法,更重要的是,我想了解为什么当前方法在本地成功而在 Jenkins 中失败。
  • Customer 中是否有任何逻辑,或者它只是一个愚蠢的数据类?如果它只是一堆带有 getter 和 setter 的字段,那么你可以实例化它。

标签: java unit-testing mockito testng


【解决方案1】:

另一种方法是使用“方法到类”模式。

  1. 将方法从客户类移到另一个类/类中,例如 CustomerSomething eg/CustomerFinances(或任何它的职责)。
  2. 向 Customer 添加一个构造函数。
  3. 现在您不需要模拟 Customer,只需模拟 CustomerSomething 类!如果它没有外部依赖项,您可能也不需要模拟它。

这是一个关于这个主题的好博客:https://simpleprogrammer.com/back-to-basics-mock-eliminating-patterns/

【讨论】:

  • 感谢您的回答 (+1)。我找到了解决它的方法(回答第二个问题)。然而,我仍然不清楚 IntelliJ 内部测试失败的原因。此外,我无法再重现它(IntelliJ 内部的故障),这非常奇怪。
【解决方案2】:

首先这怎么可能?测试不应该在本地和 Jenkins 中都失败吗?

这显然是一种环境特性。唯一的问题是——如何确定差异的原因。

我建议您检查org.mockito.internal.util.MockUtil#typeMockabilityOf 方法并比较一下mockMaker 在两种环境中实际使用的内容以及原因。

如果 mockMaker 相同 - 比较加载的类 IDE-ClientJenkins-Client - 它们在测试执行时间上是否有任何差异。

如何根据我上面提到的限制来解决这个问题?

以下代码是假设 OpenJDK 12 和 Mockito 2.28.2 编写的,但我相信您可以将其调整为任何实际使用的版本。

public class UtilityTest {    
    @Rule
    public InlineMocksRule inlineMocksRule = new InlineMocksRule();

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testFinalClass() {
        // Given
        String testName = "Ainz Ooal Gown";
        Client client = Mockito.mock(Client.class);
        Mockito.when(client.getName()).thenReturn(testName);

        // When
        String name = Utility.getName(client).orElseThrow();

        // Then
        assertEquals(testName, name);
    }

    static final class Client {
        final String getName() {
            return "text";
        }
    }

    static final class Utility {
        static Optional<String> getName(Client client) {
            return Optional.ofNullable(client).map(Client::getName);
        }
    }    
}

使用单独的内联模拟规则:

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.internal.configuration.plugins.Plugins;
import org.mockito.internal.util.MockUtil;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class InlineMocksRule implements TestRule {
    private static Field MOCK_MAKER_FIELD;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
            VarHandle modifiers = lookup.findVarHandle(Field.class, "modifiers", int.class);

            MOCK_MAKER_FIELD = MockUtil.class.getDeclaredField("mockMaker");
            MOCK_MAKER_FIELD.setAccessible(true);

            int mods = MOCK_MAKER_FIELD.getModifiers();
            if (Modifier.isFinal(mods)) {
                modifiers.set(MOCK_MAKER_FIELD, mods & ~Modifier.FINAL);
            }
        } catch (IllegalAccessException | NoSuchFieldException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Object oldMaker = MOCK_MAKER_FIELD.get(null);
                MOCK_MAKER_FIELD.set(null, Plugins.getPlugins().getInlineMockMaker());
                try {
                    base.evaluate();
                } finally {
                    MOCK_MAKER_FIELD.set(null, oldMaker);
                }
            }
        };
    }
}

【讨论】:

  • 感谢您的回答 (+1)。我找到了解决它的方法(回答第二个问题)。然而,我仍然不清楚 IntelliJ 内部测试失败的原因。此外,我无法再重现它(IntelliJ 内部的故障),这非常奇怪。
【解决方案3】:

确保使用相同的参数运行测试。检查您的 intellij 运行配置是否与 jenkins 匹配。 https://www.jetbrains.com/help/idea/creating-and-editing-run-debug-configurations.html。您可以尝试使用与 jenkins 上相同的参数(来自终端)在本地机器上运行测试,如果它会失败,则意味着问题出在参数中

【讨论】:

  • 文件org.mockito.plugins.MockMaker确实也存在于jenkins机器中。我确实在机器人机器中使用相同的 JVM。我会检查你指出的3。谢谢
  • 我尝试使用 Jenkins 中使用的命令通过控制台运行测试。它们失败并显示相同的错误消息。所以 IntelliJ 内部发生了一些奇怪的事情。
  • 在运行配置中查看 .idea/workspace.xml,它位于 标记内。之后,您可以学习如何将该 xml 转换为 bash 命令
  • 你能显示用于运行测试的 jenkins 终端命令吗?你能告诉我你用的是哪个包管理器吗?
  • 作为构建工具,我使用 Gradle。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-15
  • 1970-01-01
  • 2021-08-07
  • 2016-10-07
  • 2021-06-02
相关资源
最近更新 更多