模拟Java枚举以添加值以测试失败情况


70

我有一个或多或少像这样的枚举开关:

public static enum MyEnum {A, B}

public int foo(MyEnum value) {
    switch(value) {
        case(A): return calculateSomething();
        case(B): return calculateSomethingElse();
    }
    throw new IllegalArgumentException("Do not know how to handle " + value);
}

并且我希望测试涵盖所有行,但是由于期望代码能够处理所有可能性,因此如果没有在开关中使用其相应的case语句,则无法提供值。

扩展枚举以添加额外的值是不可能的,并且仅模拟equals方法返回false也不会起作用,因为生成的字节码使用了幕后的跳转表来进行适当处理...所以我想使用PowerMock之类的东西也许可以实现一些黑魔法。

谢谢!

编辑

当我拥有枚举时,我以为我可以在值上添加一个方法,从而完全避免切换问题。但是我仍然要提这个问题,因为它仍然很有趣。


@Melloware> ...执行switch()语句Java的代码抛出java.lang.ArrayIndexOutOfBounds ...我也有同样的问题。在测试类中首先使用新的Enum运行测试。我为此问题创建了错误:code.google.com/p/powermock/issues/detail?
id=440

当我在方法级别使用@PrepareForTest(MyEnum.class)时,它会更好地工作。
Marcin Stachniuk

1
由于枚举的清晰属性而永远不会引发的IlegalArgument,但是您该如何伪造代码以测试它可以处理不可能的事情呢?如果您真的想迷恋您的线路折中度指标,为什么不删除仅永远无法执行的线路呢?
Raedwald 2013年

13
@Raedwald 2个原因:首先,其他人可能会为枚举创建一个新值,而忘记为开关添加新的大小写;其次,如果没有throwreturn切换,代码将无法编译。
fortran

考虑之后,我认为只是未经测试。没有非法的枚举值可以触发Exception,并且模拟起来很痛苦。我认为投掷很好,这是未来的证明,真的很难测试。不值得的努力测试,恕我直言。
罗伯特·贝恩

Answers:


13

如果可以将Maven用作构建系统,则可以使用更简单的方法。只需在测试类路径中使用附加常量定义相同的枚举即可。

假设您在源目录(src / main / java)下声明了枚举,如下所示:

package my.package;

public enum MyEnum {
    A,
    B
}

现在,您可以在测试源目录(src / test / java)中声明完全相同的枚举,如下所示:

package my.package

public enum MyEnum {
    A,
    B,
    C
}

测试将看到带有“重载”枚举的testclass路径,并且您可以使用“ C”枚举常量测试代码。然后,您应该看到IllegalArgumentException。

使用Maven 3.5.2,AdoptOpenJDK 11.0.3和IntelliJ IDEA 2019.3.1在Windows下进行了测试


2
这是一个非常优雅的解决方案!我不知道那时我怎么可能会完全错过它–唯一让我担心的是,当存在重复的类定义时,依靠类路径优先级规则可能很脆弱
fortran

我没有看到gradle进行这项工作。我想在回答使用PowerMockito这里似乎是一个更好的“一般”的回答:stackoverflow.com/questions/5323505/...
布雷特·罗伊斯特

蚀似乎拒绝此解决方案The type MyEnum is already defined
wutzebaer

61

这是一个完整的例子。

该代码几乎类似于您的原始代码(只是简化了更好的测试验证):

public enum MyEnum {A, B}

public class Bar {

    public int foo(MyEnum value) {
        switch (value) {
            case A: return 1;
            case B: return 2;
        }
        throw new IllegalArgumentException("Do not know how to handle " + value);
    }
}

这是具有完整代码覆盖率的单元测试,该测试适用于Powermock(1.4.10),Mockito(1.8.5)和JUnit(4.8.2):

@RunWith(PowerMockRunner.class)
public class BarTest {

    private Bar bar;

    @Before
    public void createBar() {
        bar = new Bar();
    }

    @Test(expected = IllegalArgumentException.class)
    @PrepareForTest(MyEnum.class)
    public void unknownValueShouldThrowException() throws Exception {
        MyEnum C = mock(MyEnum.class);
        when(C.ordinal()).thenReturn(2);

        PowerMockito.mockStatic(MyEnum.class);
        PowerMockito.when(MyEnum.values()).thenReturn(new MyEnum[]{MyEnum.A, MyEnum.B, C});

        bar.foo(C);
    }

    @Test
    public void AShouldReturn1() {
        assertEquals(1, bar.foo(MyEnum.A));
    }

    @Test
    public void BShouldReturn2() {
        assertEquals(2, bar.foo(MyEnum.B));
    }
}

结果:

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.628 sec

5
我使用Mockito 1.9.0和PowerMock 1.4.12跟踪了您的示例,并且能够向列表中注入新的枚举,但是在执行switch()语句的代码中,java抛出了java.lang.ArrayIndexOutOfBounds异常,就像它知道的那样不应该是多余的。有什么想法吗?
Melloware

3
如果有人遇到@Melloware遇到的问题,以下内容可能会有用。为了使上面的示例在我自己的测试中起作用,我必须添加原始Enum的所有值,以及在when / thenReturn语句中模拟的值,并正确设置序数。如果您模拟一个额外的值,则序号应为原始未模拟的Enum中的值数。
JeroenHoek

2
您如何才能PowerMockito.mockStatic(MyEnum.class);?它应该给出java.lang.IllegalArgumentException:无法子类化最终类
Thamiar

至少对于普通的Mockito,它会引发异常。
borjab

1
优秀的!我但是我改善了解决方案。在Whitebox.setInternalState(C, "ordinal", 2);有可能通过foo替代2。并初始化fooint foo = MyEnum.values().length;第一个字符串(上方MyEnum C = PowerMockito.mock(MyEnum.class);
Alexander Skvortsov

2

与其使用某些基本的字节码操作来使测试能够击中最后一行foo,不如将其删除,而是依靠静态代码分析。例如,IntelliJ IDEA具有“缺少switch大小写的Enum语句”代码检查,foo如果该方法缺少,则将对该方法产生警告case


1
这就是为什么我将失败操作放在“默认”情况之外,也允许进行静态分析的原因……但是我不愿意删除运行时检查,而只依靠静态分析,我认为两者应该是互补的。
fortran

我的意思是,使用静态分析来补充测试套件和代码覆盖率。如果使用它,则该throw语句行将变得多余,可以删除,因为缺少caseswitch将由IDE /编译检测。
罗杰里奥

我认为这是个好主意。枚举的默认情况并非真正意义上的代码,因为它是应用程序中曾经运行过的部分,它的代码可能会在将来证明您可能会引入错误的某些事情。最好只对应用程序进行编码,并确保它是正确的。Sonar是否有“遗漏大小写的枚举切换语句”规则?
user2800708

2

正如您在编辑中指出的那样,您可以在 枚举本身中。但是,这可能不是最佳选择,因为它可能违反“一个职责”的原则。实现此目的的另一种方法是创建一个静态映射,其中包含枚举值作为键和功能作为值。这样,您可以通过遍历所有值来轻松测试任何枚举值是否具有有效的行为。在这个例子中可能有些牵强,但这是我经常用来将资源ID映射到枚举值的一种技术。


1
这没有帮助,特别是如果您没有可控制的枚举。然后,消费者可以使用具有更多值的较新版本的枚举,并且进行检查是否抛出了适当的异常或已执行默认操作的测试可能很有意义。
吸血鬼

1

jMock(至少从我正在使用的2.5.1版本开始)可以做到这一点。您需要将Mockery设置为使用ClassImposterizer。

Mockery mockery = new Mockery();
mockery.setImposterizer(ClassImposterizer.INSTANCE);
MyEnum unexpectedValue = mockery.mock(MyEnum.class);

1

仅创建伪枚举值是不够的,您最终还需要操作由编译器创建的整数数组。


实际上,要创建一个虚假的枚举值,您甚至不需要任何模拟框架。您可以只使用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表达式,例如想要100%覆盖default,要使枚举像您的示例那样被扩展,则抛出异常的情况情况将变得更加复杂,同时也更加容易。

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

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

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

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

基本上总结起来,在字节码级别启用开关不适用于枚举,而仅适用于整数。因此,编译器要做的是,它创建一个匿名内部类(根据本文,以前是一个命名的内部类,这是Java 6 vs. Java 8),其中包含一个静态的finalint[]字段$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);
}

如果现在myEnumVariableNON_EXISTENT在上面的第一步中创建该值,则在两种情况下,ArrayIndexOutOfBoundsException如果设置的ordinal值大于编译器生成的数组,则将获得一个,否则,将获得其他切换条件值之一这无助于测试通缉default案件。

现在,您可以获取此int[]字段并对其进行修复,以包含NON_EXISTENT枚举实例的序数的映射。但是正如我之前所说的,对于这个用例来说,测试用例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(),但这将再次导致不同的开关分支或AIOOBE。

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

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

0

首先,Mockito可以创建可以为整数长的模拟数据等,因为枚举具有特定数量的序数名称值等,因此无法创建正确的枚举,所以如果我有枚举

public enum HttpMethod {
      GET, POST, PUT, DELETE, HEAD, PATCH;
}

所以我在枚举HttpMethod中总共有5个序数,但是mockito不知道它.Mockito始终创建模拟数据及其null,最终会传递null值。因此,这里提出了一种解决方案,您可以对序数进行随机化并获得正确的枚举,该枚举可以通过其他测试

import static org.mockito.Mockito.mock;

import java.util.Random;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.internal.util.reflection.Whitebox;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.amazonaws.HttpMethod;




//@Test(expected = {"LoadableBuilderTestGroup"})
//@RunWith(PowerMockRunner.class)
public class testjava {
   // private static final Class HttpMethod.getClass() = null;
    private HttpMethod mockEnumerable;

    @Test
    public void setUpallpossible_value_of_enum () {
        for ( int i=0 ;i<10;i++){
            String name;
            mockEnumerable=    Matchers.any(HttpMethod.class);
            if(mockEnumerable!= null){
                System.out.println(mockEnumerable.ordinal());
                System.out.println(mockEnumerable.name());

                System.out.println(mockEnumerable.name()+"mocking suceess");
            }
            else {
                //Randomize all possible  value of  enum 
                Random rand = new Random();
                int ordinal = rand.nextInt(HttpMethod.values().length); 
                // 0-9. mockEnumerable=
                mockEnumerable= HttpMethod.values()[ordinal];
                System.out.println(mockEnumerable.ordinal());
                System.out.println(mockEnumerable.name());
            }
        }
    }







    @Test
    public void setUpallpossible_value_of_enumwithintany () {
        for ( int i=0 ;i<10;i++){
            String name;
            mockEnumerable=    Matchers.any(HttpMethod.class);
            if(mockEnumerable!= null){
                System.out.println(mockEnumerable.ordinal());
                System.out.println(mockEnumerable.name());

                System.out.println(mockEnumerable.name()+"mocking suceess");
            } else {
               int ordinal;
               //Randomize all possible  value of  enum 
               Random rand = new Random();
               int imatch =  Matchers.anyInt();
               if(  imatch>HttpMethod.values().length)
                 ordinal = 0    ;
               else
                ordinal = rand.nextInt(HttpMethod.values().length);

               // 0-9.  mockEnumerable=
               mockEnumerable= HttpMethod.values()[ordinal];
               System.out.println(mockEnumerable.ordinal());
               System.out.println(mockEnumerable.name());       
            }
       }  
    }
}

输出:

0
GET
0
GET
5
PATCH
5
PATCH
4
HEAD
5
PATCH
3
DELETE
0
GET
4
HEAD
2
PUT

0

我认为达​​到IllegalArgumentException的最简单方法是将null传递给foo方法,您将看到“不知道如何处理null”


2
这一点都没有帮助,因为switch对一个null值执行ing会产生aNullPointerException而不是遵循default大小写(在示例中,它会落到switch语句之后
Vampire

0

我在枚举中添加了一个Unknown选项,该选项在测试过程中通过。在每种情况下都不理想,但是简单。


-10

我将默认情况与枚举情况之一:

  public static enum MyEnum {A, B}

  public int foo(MyEnum value) {
    if (value == null) throw new IllegalArgumentException("Do not know how to handle " + value);

    switch(value) {
        case(A):
           return calculateSomething();
        case(B):
        default:
           return calculateSomethingElse();
    }
  }

15
这不是期望的行为,当添加新的枚举值时,它将产生意外的结果。
fortran
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.