如何使用JMockit模拟/伪造新的枚举值

时间:2016-08-24 05:23:26

标签: java unit-testing junit enums jmockit

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

我无法在文档中找到任何内容。它甚至可能吗?

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

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

3 个答案:

答案 0 :(得分: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语句错过case”)或静态分析工具中的规则。

但是,最好的解决方案是将与枚举元素相关联的常量值(枚举本身)放在一起,因此无需switch

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

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

答案 1 :(得分:0)

对这个问题进行了第二次思考后,我找到了一个解决方案,而且令人惊讶的是一个非常微不足道的解决方案。

您正在询问有关模拟枚举或在测试中扩展它的问题。但实际问题实际上似乎是必须保证任何枚举扩展必须伴随着使用它的函数的修正。所以你基本上需要一个测试,如果枚举被扩展,它将失败,无论是否使用了模拟或者根本不可能。事实上,如果可能的话,最好不要去。

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

原始枚举:

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

答案 2 :(得分:0)

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


实际上,要创建一个虚假的枚举值,您甚至不需要任何模拟框架。您可以仅使用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表达式,但使用某些if或类似表达式,则仅第一部分或第一和第二部分就足够了。 / p>

但是,如果您要处理switch表达式,请执行e。 G。想要为default案例提供100%的覆盖率,以防枚举扩展,情况变得更复杂,同时更容易。

稍微复杂一点,因为您需要做一些认真的思考才能操作编译器在编译器生成的合成匿名innner类中生成的合成字段,因此您所做的事情并不一定很明显编译器的实际实现,因此这可能会在任何Java版本中的任何时间中断,即使您对同一Java版本使用不同的编译器也是如此。实际上,Java 6和Java 8之间已经有所不同。

更容易一些,因为您可以忘记此答案的前两个部分,因为根本不需要创建新的枚举实例,只需要操作一个int[]无论如何都要进行操作以进行所需的测试。

我最近在https://www.javaspecialists.eu/archive/Issue161.html找到了一篇很好的文章。

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

基本总结,字节码级别的开关不适用于枚举,而仅适用于整数。因此,编译器要做的是,它创建一个匿名内部类(根据本文撰写,以前是一个命名内部类,这是Java 6 vs. Java 8),其中包含一个称为{{1}的静态final int[] },它在$SwitchMap$net$kautler$MyEnum值的索引处填充有整数1、2、3,...。

这意味着当代码进入实际的开关时,它会

MyEnum#ordinal()

如果现在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的值设置为大于{数组,编译器生成的数组,否则,您将获得其他切换用例值之一,在这两种情况下,这都无助于测试所需的ArrayIndexOutOfBoundsException用例。

您现在可以获取此ordinal字段并将其修正为包含您的default枚举实例的序数的映射。但是正如我之前所说,对于这个用例,测试int[]情况,您根本不需要前两个步骤。相反,您可以简单地将任何现有的枚举实例赋予要测试的代码,并只需操纵映射NON_EXISTENT,即可触发default情况。

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

int[]

在这种情况下,您根本不需要任何模拟框架。实际上,无论如何它还是无济于事,因为我所知道的模拟框架都不允许您模拟数组访问。您可以使用JMockit或任何模拟框架来模拟default的返回值,但这将再次导致不同的切换分支或AIOOBE。

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

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