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

for*_*ran 50 java enums unit-testing code-coverage mocking

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

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);
}
Run Code Online (Sandbox Code Playgroud)

并且我希望测试涵盖所有行,但由于代码应该处理所有可能性,因此我无法在交换机中提供没有相应case语句的值.

扩展枚举以添加额外的值是不可能的,只是模拟返回的equals方法false将无法工作,因为生成的字节码使用窗帘后面的跳转表来找到正确的情况......所以我想也许用PowerMock可以实现一些黑魔法.

谢谢!

编辑:

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

Jon*_*eim 51

这是一个完整的例子.

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这里是完整代码覆盖的单元测试,测试适用于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));
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.628 sec
Run Code Online (Sandbox Code Playgroud)

  • 我使用Mockito 1.9.0和PowerMock 1.4.12跟随你的例子,我能够在我的列表中注入一个新的枚举,但是在执行switch()语句的代码中,java会抛出一个java.lang.ArrayIndexOutOfBounds异常,就像它知道那里一样不应该是额外的.有什么想法吗? (5认同)
  • 如果有人遇到@Melloware的问题,以下内容可能会有用.为了让上面的例子在我自己的测试中工作,我必须在when/thenReturn语句中添加原始枚举的所有值和模拟的值,并正确设置序数.如果您模拟一个额外的值,则序数应该是原始未模拟的枚举中的值的数量. (3认同)
  • 你怎么能PowerMockito.mockStatic(MyEnum.class);? 它应该给java.lang.IllegalArgumentException:不能继承final类 (2认同)

小智 17

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

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

package my.package;

public enum MyEnum {
    A,
    B
}
Run Code Online (Sandbox Code Playgroud)

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

package my.package

public enum MyEnum {
    A,
    B,
    C
}
Run Code Online (Sandbox Code Playgroud)

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

在 windows 下使用 maven 3.5.2、AdoptOpenJDK 11.0.3 和 IntelliJ IDEA 2019.3.1 进行测试

  • 这是一个非常优雅的解决方案!我不知道当时我怎么会完全错过它,它只让我担心,当存在重复的类定义时依赖类路径优先级规则可能是脆弱的 (2认同)

Nan*_*oka 8

这是我的 Mockito 版本的 @Jonny Heggheim 的解决方案。它已经使用 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

使用此功能时请注意以下几点:

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