模拟Java枚举以添加值以测试失败案例

时间:2011-03-16 09:50:35

标签: java unit-testing enums mocking code-coverage

我的 enum 开关或多或少都是这样的:

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或其他东西可以实现一些黑魔法。

谢谢!

修改

由于我拥有枚举,我认为我可以只为值添加一个方法,从而完全避免切换问题;但是我要离开这个问题因为它仍然很有趣。

12 个答案:

答案 0 :(得分:45)

这是一个完整的例子。

代码几乎与您的原始代码一样(只是简化了更好的测试验证):

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 = PowerMockito.mock(MyEnum.class);
        Whitebox.setInternalState(C, "name", "C");
        Whitebox.setInternalState(C, "ordinal", 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

答案 1 :(得分:13)

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

假设您将枚举声明在sources目录(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。

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

答案 2 :(得分:6)

@Melloware

  

...执行switch()语句的代码java抛出java.lang.ArrayIndexOutOfBounds ...

我有同样的问题。使用新的Enum作为测试类中的第一个运行测试。我创建了这个问题的bug:https://code.google.com/p/powermock/issues/detail?id=440

答案 3 :(得分:2)

不是使用一些激进的字节码操作来使测试能够到达foo中的最后一行,而是将其删除并依赖于静态代码分析。例如,IntelliJ IDEA具有“枚举switch语句错过案例”代码检查,如果foo方法缺少case,则会生成{{1}}方法的警告。

答案 4 :(得分:2)

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

答案 5 :(得分:1)

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

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

答案 6 :(得分: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表达式,但使用某些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块中),以确保这不会影响同一类的其他测试,即原始切换图重新放回切换图字段

答案 7 :(得分:0)

首先,Mockito可以创建可以是整数等的模拟数据 它不能创建正确的枚举,因为枚举具有特定数量的序号 价值等等,如果我有一个枚举

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

所以我在枚举HttpMethod中总共有5个序数,但是mockito不知道它.Mockito一直创建模拟数据及其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

答案 8 :(得分:0)

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

答案 9 :(得分:0)

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

答案 10 :(得分:0)

这是@Jonny Heggheim 解决方案的 Mockito 唯一版本。它已经用 Mockito 3.9.0 和 Java 11 测试过:

public class MyTestClass {

  private static MockedStatic<MyEnum> myMockedEnum;
  private static MyEnum mockedValue;

  @BeforeClass
  public void setUp() {
    MyEnum[] newEnumValues = addNewEnumValue(MyEnum.class);
    myMockedEnum = mockStatic(MyEnum.class);
    myMockedEnum.when(MyEnum::values).thenReturn(newEnumValues);
    mockedValue = newEnumValues[newEnumValues.length - 1];
  }
 
  @AfterClass
  public void tearDown(){
    myMockedEnum.close();
  }

  @Test
  public void testCase(){
    // Use mockedValue in your test case
    ...
  }

  private static <E extends Enum<E>> E[] addNewEnumValue(Class<E> enumClazz){
    EnumSet<E> enumSet = EnumSet.allOf(enumClazz);
    E[] newValues = (E[]) Array.newInstance(enumClazz, enumSet.size() + 1);
    int i = 0;
    for (E value : enumSet) {
      newValues[i] = value;
      i++;
    }

    E newEnumValue = mock(enumClazz);
    newValues[newValues.length - 1] = newEnumValue;

    when(newEnumValue.ordinal()).thenReturn(newValues.length - 1);

    return newValues;
  }
}

使用时的一些注意事项:

  • 在包含模拟枚举的 switch 语句的 JVM 类加载器加载任何类之前,在 setup() 方法中运行代码至关重要。如果您想知道原因,我建议您阅读@Vampire 的回答中引用的文章。
  • 实现此目的最安全的方法是将代码放入使用 @BeforeClass 注释的静态方法中。
  • 如果您忘记了 tearDown() 方法中的代码,如果测试类中的测试成功但其他测试类中的测试随后在同一测试运行中运行时可能会失败。这是因为 MyEnum 会一直保持延长,直到您在 close() 上调用 MockedStatic
  • 如果在同一个测试类中,有些测试用例使用模拟的 Enum 而有些不使用,并且您必须将 setUp()tearDown() 代码提取到单个测试用例中,我强烈建议运行使用 Robolectric 运行器或任何其他测试运行器进行的测试,保证每个测试用例在新启动的 JVM 中运行。通过这种方式,您可以确保所有包含 Enum 的 switch 语句的类都是由类加载器为每个测试用例新加载的。

答案 11 :(得分:-8)

我会在默认情况下使用枚举案例之一:

  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();
    }
  }