【问题标题】:How to mock/fake a new enum value with JMockit如何使用 JMockit 模拟/伪造一个新的枚举值
【发布时间】:2016-12-31 02:23:20
【问题描述】:

如何使用 JMockit 添加虚假枚举值?

我在文档中找不到任何内容。有没有可能?

相关:this question 但它仅适用于 mockito,不适用于 JMockIt。

编辑:我首先删除了我给出的示例,因为这些示例似乎让人分心。请查看linked question 上最受好评的答案,看看我在期待什么。我想知道是否可以用 JMockit 做同样的事情。

【问题讨论】:

  • @dorukayhan 重复?我已经明确链接了“重复”问题,并说它没有回答这个问题,因为它使用的是 Mockito 而不是 JMockit。不知道我还能说什么来证明它不是重复的。
  • 等等,这是我的错 - 我没有看到 The link is only for mockito 部分。对不起。
  • 您的“附加”情况不是很清楚:您没有在这里抛出异常,并且映射不必包含所有枚举条目的值,因此与任何额外的枚举值没有直接关系或相应的测试。您实际上问的问题与您所指的问题有什么不同?
  • @OlegSklyar 它实际上是相同的情况,但这表明我想添加一个新的枚举值,而不是通过开关,因为我也可以使用地图。是的,地图可能不包含所有元素,但添加假元素将确保我点击“//访问此处”行。

标签: java unit-testing junit enums jmockit


【解决方案1】:

我认为您正在尝试解决错误的问题。相反,修复foo(MyEnum) 方法如下:

    public int foo(MyEnum value) {
        switch (value) {
            case A: return 1;
            case B: return 2;
        }
        return 0; // just to satisfy the compiler
    }

最后有一个throw 来捕获一个不存在的假想枚举元素是没有用的,因为它永远不会到达。如果您担心将新元素添加到枚举中并且 foo 方法没有相应更新,那么有更好的解决方案。其中之一是依赖于 Java IDE 的代码检查(IntelliJ 至少有一个用于这种情况:“switch statement on enumerated type misses case”)或来自静态分析工具的规则。

不过,最好的解决方案是将与枚举元素关联的常量值放在它们所属的位置(枚举本身),因此无需switch

public enum BetterEnum {
    A(1), B(2);

    public final int value;
    BetterEnum(int value) { this.value = value; }
}

【讨论】:

  • 如果其他人修改了不应该在不更改下游代码的情况下修改的结构,那么依靠代码检查比防止返回不正确的值更好吗?这个人甚至不知道他应该检查代码检查......
  • 您正在尝试解决我给出的第一个示例,而不是问题,我不同意您的回答并同意@OlegSklyar 评论。那是因为人们并不总是运行代码检查,我想要一种正确进行单元测试的方法。我不想依赖代码检查工具来防止错误。
  • 看你是JMockit的创造者,估计是不可能的。您能否使用 JMockit 更改您的答案以让我知道是否可行?不管你是否认为这是个好主意。
  • @OlegSklyar 您没有注意到 OP 想要的测试也无法解决问题。即使编写了该测试,当有人将元素“C”添加到枚举中时,也不能保证他们会使用switch 将其添加到方法中。他们要么调用foo(C) 得到异常,要么返回无效的返回值。这个细节对我的回答并不重要——关键是测试没有价值。
  • 另外,请注意我推荐的“最佳解决方案”确实可以解决问题,因为它需要添加到枚举中的任何新元素都获得其相关值。而且它不依赖于任何代码检查。
【解决方案2】:

重新考虑了这个问题后,我有了一个解决方案,而且令人惊讶的是一个非常简单的解决方案。

您是在询问模拟枚举或在测试中扩展它。但实际问题似乎是必须保证任何枚举扩展都必须伴随着对使用它的函数的修改。因此,无论是否使用模拟或完全可能,您基本上都需要一个测试,如果枚举被扩展,它将失败。事实上,如果可能的话,最好不要这样做。

我多次遇到完全相同的问题,但在看到您的问题后,我才想到了实际的解决方案:

原始枚举:

public enum MyEnum { A, B }

枚举只提供AB时已经定义的函数:

public int mapper(MyEnum e) {
  switch (e) {
    case A: return 1;
    case B: return 2;
    default:
      throw new IllegalArgumentException("value not supported");
  }
}

将指出mapper在扩展枚举时需要处理的测试:

@Test
public void test_mapper_onAllDefinedArgValues_success() {
  for (MyEnum e: MyEnum.values()) {
    mapper(e);
  }
}

测试结果:

Process finished with exit code 0

现在让我们用新值 C 扩展枚举并重新运行测试:

java.lang.IllegalArgumentException: value not supported

at io.ventu.rpc.amqp.AmqpResponderTest.mapper(AmqpResponderTest.java:104)
...
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Process finished with exit code 255

【讨论】:

  • 这将测试是否处理了所有枚举值,但不会测试可能添加的枚举值会引发异常。而且这也不会为您带来 100% 的分支覆盖率甚至 100% 的线路覆盖率。例如,如果枚举来自外部代码,则在扩展枚举时测试可能不会运行,因此这不是一个合适的有用答案。
【解决方案3】:

仅仅创建一个假的枚举值可能还不够,你最终还需要操作一个由编译器创建的整数数组。


实际上要创建一个假枚举值,您甚至不需要任何模拟框架。您可以使用 Objenesis 创建枚举类的新实例(是的,这可行),然后使用普通的旧 Java 反射来设置私有字段 nameordinal,并且您已经有了新的枚举实例。

使用 Spock 框架进行测试,如下所示:

given:
    def getPrivateFinalFieldForSetting = { clazz, fieldName ->
        def result = clazz.getDeclaredField(fieldName)
        result.accessible = true
        def modifiers = Field.getDeclaredFields0(false).find { it.name == 'modifiers' }
        modifiers.accessible = true
        modifiers.setInt(result, result.modifiers & ~FINAL)
        result
    }

and:
    def originalEnumValues = MyEnum.values()
    MyEnum NON_EXISTENT = ObjenesisHelper.newInstance(MyEnumy)
    getPrivateFinalFieldForSetting.curry(Enum).with {
        it('name').set(NON_EXISTENT, "NON_EXISTENT")
        it('ordinal').setInt(NON_EXISTENT, originalEnumValues.size())
    }

如果您还希望 MyEnum.values() 方法返回新枚举,您现在可以使用 JMockit 模拟 values() 调用,如

new MockUp<MyEnum>() {
    @Mock
    MyEnum[] values() {
        [*originalEnumValues, NON_EXISTENT] as MyEnum[]
    }
}

或者您可以再次使用普通的旧反射来操作 $VALUES 字段,例如:

given:
    getPrivateFinalFieldForSetting.curry(MyEnum).with {
        it('$VALUES').set(null, [*originalEnumValues, NON_EXISTENT] as MyEnum[])
    }

expect:
    true // your test here

cleanup:
    getPrivateFinalFieldForSetting.curry(MyEnum).with {
        it('$VALUES').set(null, originalEnumValues)
    }

只要您不处理 switch 表达式,而是处理一些 ifs 或类似的表达式,那么仅第一部分或第一和第二部分对您来说可能就足够了。

但是,如果您正在处理 switch 表达式,例如。 G。想要 100% 覆盖 default 的情况,如果枚举扩展会引发异常,事情会变得更复杂一些,同时也更容易一些。

有点复杂,因为您需要进行一些认真的反射来操作编译器在编译器生成的合成匿名内部类中生成的合成字段,因此您在做什么并不明显并且您一定会编译器的实际实现,因此这可能会在任何 Java 版本中随时中断,或者即使您对相同的 Java 版本使用不同的编译器。 Java 6 和 Java 8 实际上已经不同了。

简单一点,因为你可以忘记这个答案的前两部分,因为你根本不需要创建一个新的枚举实例,你只需要操作一个int[],你需要操作无论如何做你想要的测试。

我最近在https://www.javaspecialists.eu/archive/Issue161.html 发现了一篇非常好的文章。

那里的大部分信息仍然有效,只是现在包含开关映射的内部类不再是命名内部类,而是匿名类,因此您不能再使用getDeclaredClasses,而是需要使用不同的方法如下所示。

基本上总结,字节码级别的开关不适用于枚举,而仅适用于整数。所以编译器所做的是,它创建一个匿名内部类(根据文章写作,以前是一个命名的内部类,这是 Java 6 与 Java 8),它包含一个名为 $SwitchMap$net$kautler$MyEnum 的静态最终 int[] 字段,该字段已填充在 MyEnum#ordinal() 值的索引处使用整数 1、2、3、...。

这意味着当代码到达实际的开关时,它确实

switch(<anonymous class here>.$SwitchMap$net$kautler$MyEnum[myEnumVariable.ordinal()]) {
    case 1: break;
    case 2: break;
    default: throw new AssertionError("Missing switch case for: " + myEnumVariable);
}

如果现在myEnumVariable 将具有在上述第一步中创建的值NON_EXISTENT,如果您将ordinal 设置为大于编译器生成的数组的某个值,您将获得ArrayIndexOutOfBoundsException,或者您将如果没有,则获取其他 switch-case 值之一,在这两种情况下,这都无助于测试所需的 default 案例。

您现在可以获取此 int[] 字段并将其修复为包含 NON_EXISTENT 枚举实例的 orinal 的映射。但正如我之前所说,对于这个用例,测试default 案例,您根本不需要前两个步骤。相反,您可以简单地将任何现有枚举实例提供给被测代码,并简单地操作映射int[],从而触发default 案例。

所以这个测试用例所需要的实际上就是这个,再次用 Spock (Groovy) 代码编写,但您也可以轻松地将其改编为 Java:

given:
    def getPrivateFinalFieldForSetting = { clazz, fieldName ->
        def result = clazz.getDeclaredField(fieldName)
        result.accessible = true
        def modifiers = Field.getDeclaredFields0(false).find { it.name == 'modifiers' }
        modifiers.accessible = true
        modifiers.setInt(result, result.modifiers & ~FINAL)
        result
    }

and:
    def switchMapField
    def originalSwitchMap
    def namePrefix = ClassThatContainsTheSwitchExpression.name
    def classLoader = ClassThatContainsTheSwitchExpression.classLoader
    for (int i = 1; ; i++) {
        def clazz = classLoader.loadClass("$namePrefix\$$i")
        try {
            switchMapField = getPrivateFinalFieldForSetting(clazz, '$SwitchMap$net$kautler$MyEnum')
            if (switchMapField) {
                originalSwitchMap = switchMapField.get(null)
                def switchMap = new int[originalSwitchMap.size()]
                Arrays.fill(switchMap, Integer.MAX_VALUE)
                switchMapField.set(null, switchMap)
                break
            }
        } catch (NoSuchFieldException ignore) {
            // try next class
        }
    }

when:
    testee.triggerSwitchExpression()

then:
    AssertionError ae = thrown()
    ae.message == "Unhandled switch case for enum value 'MY_ENUM_VALUE'"

cleanup:
    switchMapField.set(null, originalSwitchMap)

在这种情况下,您根本不需要任何模拟框架。实际上它无论如何都不会帮助你,因为我知道没有任何模拟框架允许你模拟数组访问。您可以使用 JMockit 或任何模拟框架来模拟 ordinal() 的返回值,但这会再次导致不同的 switch-branch 或 AIOOBE。

我刚刚展示的这段代码的作用是:

  • 它循环遍历包含 switch 表达式的类中的匿名类
  • 在搜索字段中使用 switch 映射
  • 如果没有找到该字段,则尝试下一个类
  • 如果ClassNotFoundExceptionClass.forName 抛出,测试失败,这是有意的,因为这意味着您使用遵循不同策略或命名模式的编译器编译代码,因此您需要添加更多智能涵盖用于打开枚举值的不同编译器策略。因为如果找到带有该字段的类,break 就会离开 for 循环,测试可以继续。当然,整个策略取决于匿名类从 1 开始编号并且没有间隙,但我希望这是一个非常安全的假设。如果您处理的编译器并非如此,则需要相应地调整搜索算法。
  • 如果找到 switch map 字段,则创建一个新的相同大小的 int 数组
  • 新数组填充有 Integer.MAX_VALUE,只要您没有包含 2,147,483,647 个值的枚举,通常应该触发 default 情况
  • 将新数组分配给切换映射字段
  • 使用break 留下for 循环
  • 现在可以完成实际测试,触发要评估的 switch 表达式
  • 最后(如果您不使用 Spock,则在 finally 块中,如果您正在使用 Spock,则在 cleanup 块中)以确保这不会影响同一类上的其他测试,放置原始切换映射回到切换地图字段

【讨论】:

    猜你喜欢
    • 2023-03-04
    • 1970-01-01
    • 2022-07-07
    • 1970-01-01
    • 2013-09-03
    • 2022-01-16
    • 2018-09-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多