Mockito嘲笑本地最终课程,但在Jenkins中失败

2022-09-04 23:06:52

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

public class Utility {

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

public final class Customer {
   // class definition
}

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

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: Can not mock/spy class com.packagename.Customer Mockito can no mock/spy,因为 : - final class

我试过了什么?

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

问题

所以这里有两个问题:

  1. 这首先是如何实现的?测试不应该在本地和 Jenkins 中都失败吗?
  2. 如何根据我上面提到的约束来解决这个问题?

提前感谢您的任何帮助。


答案 1

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

  1. 将方法从客户类移动到另一个类/类中,例如 CustomerSomething(或它的责任是什么)。
  2. 将构造函数添加到客户。
  3. 现在你不需要嘲笑客户,只需要嘲笑客户一些类!如果它没有外部依赖关系,您可能也不需要模拟它。

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


答案 2

这首先是如何实现的呢?测试不应该在本地和 Jenkins 中都失败吗?

这显然是一种环境细节。唯一的问题是 - 如何确定差异的原因。

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

如果是相同的 - 比较装入的类与 - 它们在测试执行时间上是否有任何差异。mockMakerIDE-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);
                }
            }
        };
    }
}

推荐