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

发布于 2024-10-22 02:32:49 字数 617 浏览 3 评论 0原文

我有一个 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);
}

我希望测试涵盖所有行,但由于代码预计会处理所有可能性,因此我无法提供值switch 中没有相应的 case 语句。

扩展枚举来添加额外的值是不可能的,并且仅仅模拟 equals 方法返回 false 也不起作用,因为生成的字节码在幕后使用跳转表来转到正确的情况...所以我想也许可以通过 PowerMock 之类的东西来实现一些黑魔法。

谢谢!

编辑

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

I have an enum switch more or less like this:

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

and I'd like to have all the lines covered by the tests, but as the code is expected to deal with all possibilities, I cannot supply a value without its corresponding case statement in the switch.

Extending the enum to add an extra value is not possible, and just mocking the equals method to return false won't work either because the bytecode generated uses a jump table behind the curtains to go to the proper case... So I've thought that maybe some black magic could be achieved with PowerMock or something.

Thanks!

edit:

As I own the enumeration, I've thought that I could just add a method to the values and thus avoid the switch issue completely; but I'm leaving the question as it's still interesting.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

梦与时光遇 2024-10-29 02:32:49

这是一个完整的例子。

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

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

Here is a complete example.

The code is almost like your original (just simplified better test validation):

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

And here is the unit test with full code coverage, the test works with Powermock (1.4.10), Mockito (1.8.5) and 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));
    }
}

Result:

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.628 sec
小嗷兮 2024-10-29 02:32:49

如果您可以使用 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 。

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

If you can use Maven as your build system, you can use a much simpler approach. Just define the same enum with an additional constant in your test classpath.

Let's say you have your enum declared under the sources directory (src/main/java) like this:

package my.package;

public enum MyEnum {
    A,
    B
}

Now you declare the exact same enum in the test sources directory (src/test/java) like this:

package my.package

public enum MyEnum {
    A,
    B,
    C
}

The tests see the testclass path with the "overloaded" enum and you can test your code with the "C" enum constant. You should see your IllegalArgumentException then.

Tested under windows with maven 3.5.2, AdoptOpenJDK 11.0.3 and IntelliJ IDEA 2019.3.1

指尖上的星空 2024-10-29 02:32:49

这是我的 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;
  }
}

使用此方法时需要注意几点:

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

Here is my Mockito only version of the solution of @Jonny Heggheim. It has been tested with Mockito 3.9.0 and 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;
  }
}

A few words of caution when using this:

  • It is crucial that you run the code in the setup() Method before any class is loaded by the JVM classloader that contains a switch statement for the mocked Enum. I recommend you read the article quoted in the answer of @Vampire if you want to know why.
  • The safest way to achieve this is to put the code in a static method annotated with @BeforeClass.
  • If you forget the code in the tearDown() method if can happen that the tests in the test class succeed but tests in other test classes fail when run afterwards in the same test run. This is because MyEnum stays extended until you call close() on MockedStatic.
  • If in the same test class some of your test cases use the mocked Enum and some don't and you have to pull the setUp() and tearDown() code into single test cases, I highly recommend running the tests with the Robolectric runner or any other test runner, that guarantees that each test case runs in a freshly started JVM. This way you can make sure, that all classes that contain switch statements for the Enum are newly loaded by the class loader for every test case.
猫腻 2024-10-29 02:32:49

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


实际上,要创建一个假枚举值,您甚至不需要任何模拟框架。您可以使用 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 来模拟 value( ) 像这样调用

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 或类似的情况,仅第一部分或第一和第二部分可能对您来说就足够了。

但是,如果您正在处理 switch 表达式,例如希望 default 情况的 100% 覆盖率,该情况会引发异常,以防枚举像您的示例中那样扩展,事情会变得稍微复杂一点,同时又简单一点。

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

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

我最近在 https://www.javaspecialists.eu/archive/ 找到了一篇关于此的非常好的文章Issue161.html

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

基本上总结一下,字节码级别的开关不适用于枚举,而仅适用于整数。因此,编译器所做的是,它创建一个匿名内部类(以前是根据本文撰写的命名内部类,这是 Java 6 与 Java 8),该内部类包含一个静态最终 int[] 字段称为 $SwitchMap$net$kautler$MyEnum,在 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,您将获得一个 ArrayIndexOutOfBoundsException如果您将ordinal设置为大于编译器生成的数组的某个值,或者如果不是,您将获得其他switch-case值之一,在这两种情况下这都无济于事测试所需的默认情况。

您现在可以获取此 int[] 字段并对其进行修复,以包含 NON_EXISTENT 枚举实例的 orinal 的映射。但正如我之前所说,对于这个用例,测试默认情况,您根本不需要前两个步骤。相反,您可以简单地将任何现有枚举实例提供给正在测试的代码,并简单地操作映射 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 表达式的类中的匿名类,
  • 则使用 switch 映射搜索该字段
  • 如果未找到该字段,
  • ,如果 ClassNotFoundExceptionClass.forName 抛出,测试失败,这是有意的,因为这意味着您使用遵循不同策略或命名模式的编译器编译代码,因此您需要添加更多智能来涵盖用于切换枚举值的不同编译器策略。因为如果找到具有该字段的类,break 就会离开 for 循环,测试可以继续。当然,整个策略取决于匿名类从 1 开始编号并且没有间隙,但我希望这是一个非常安全的假设。如果您使用的编译器并非如此,则需要相应地调整搜索算法。
  • 如果找到 switch 映射字段,则会创建一个相同大小的新 int 数组,
  • 新数组将填充 Integer.MAX_VALUE,通常应该触发 default 情况,只要由于您没有包含 2,147,483,647 个值的枚举,因此
  • 新数组被分配给 switch 映射字段,
  • 使用 break 保留 for 循环
  • ,现在可以完成实际测试,触发要评估的 switch 表达式
  • 最后(如果您不使用 Spock,则在 finally 块中;如果您使用 Spock,则在 cleanup 块中)以确保这不会影响同一类上的其他测试,将原来的switch map放回switch map字段

Just creating a fake enum value will not be enough, you eventually also need to manipulate an integer array that is created by the compiler.


Actually to create a fake enum value, you don't even need any mocking framework. You can just use Objenesis to create a new instance of the enum class (yes, this works) and then use plain old Java reflection to set the private fields name and ordinal and you already have your new enum instance.

Using Spock framework for testing, this would look something like:

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

If you also want the MyEnum.values() method to return the new enum, you now can either use JMockit to mock the values() call like

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

or you can again use plain old reflection to manipulate the $VALUES field like:

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

As long as you don't deal with a switch expression, but with some ifs or similar, either just the first part or the first and second part might be enough for you.

If you however are dealing with a switch expression, e. g. wanting 100% coverage for the default case that throws an exception in case the enum gets extended like in your example, things get a bit more complicated and at the same time a bit more easy.

A bit more complicated because you need to do some serious reflection to manipulate a synthetic field that the compiler generates in a synthetic anonymous innner class that the compiler generates, so it is not really obvious what you are doing and you are bound to the actual implementation of the compiler, so this could break anytime in any Java version or even if you use different compilers for the same Java version. It is actually already different between Java 6 and Java 8.

A bit more easy, because you can forget the first two parts of this answer, because you don't need to create a new enum instance at all, you just need to manipulate an int[], that you need to manipulate anyway to make the test you want.

I recently found a very good article regarding this at https://www.javaspecialists.eu/archive/Issue161.html.

Most of the information there is still valid, except that now the inner class containing the switch map is no longer a named inner class, but an anonymous class, so you cannot use getDeclaredClasses anymore but need to use a different approach shown below.

Basically summarized, switch on bytecode level does not work with enums, but only with integers. So what the compiler does is, it creates an anonymous inner class (previously a named inner class as per the article writing, this is Java 6 vs. Java 8) that holds one static final int[] field called $SwitchMap$net$kautler$MyEnum that is filled with integers 1, 2, 3, ... at the indices of MyEnum#ordinal() values.

This means when the code comes to the actual switch, it does

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

If now myEnumVariable would have the value NON_EXISTENT created in the first step above, you would either get an ArrayIndexOutOfBoundsException if you set ordinal to some value greater than the array the compiler generated, or you would get one of the other switch-case values if not, in both cases this would not help to test the wanted default case.

You could now get this int[] field and fix it up to contain a mapping for the orinal of your NON_EXISTENT enum instance. But as I said earlier, for exactly this use-case, testing the default case, you don't need the first two steps at all. Instead you can simple give any of the existing enum instances to the code under test and simply manipulate the mapping int[], so that the default case is triggered.

So all that is necessary for this test case is actually this, again written in Spock (Groovy) code, but you can easily adapt it to Java too:

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)

In this case you don't need any mocking framework at all. Actually it would not help you anyway, as no mocking framework I'm aware of allows you to mock an array access. You could use JMockit or any mocking framework to mock the return value of ordinal(), but that would again simply result in a different switch-branch or an AIOOBE.

What this code I just shown does is:

  • it loops through the anonymous classes inside the class that contains the switch expression
  • in those it searches for the field with the switch map
  • if the field is not found, the next class is tried
  • if a ClassNotFoundException is thrown by Class.forName, the test fails, which is intended, because that means that you compiled the code with a compiler that follows a different strategy or naming pattern, so you need to add some more intelligence to cover different compiler strategies for switching on enum values. Because if the class with the field is found, the break leaves the for-loop and the test can continue. This whole strategy of course depends on anonymous classes being numbered starting from 1 and without gaps, but I hope this is a pretty safe assumption. If you are dealing with a compiler where this is not the case, the searching algorithm needs to be adapted accordingly.
  • if the switch map field is found, a new int array of the same size is created
  • the new array is filled with Integer.MAX_VALUE which usually should trigger the default case as long as you don't have an enum with 2,147,483,647 values
  • the new array is assigned to the switch map field
  • the for loop is left using break
  • now the actual test can be done, triggering the switch expression to be evaluated
  • finally (in a finally block if you are not using Spock, in a cleanup block if you are using Spock) to make sure this does not affect other tests on the same class, the original switch map is put back into the switch map field
扬花落满肩 2024-10-29 02:32:49

我不会使用一些激进的字节码操作来使测试能够命中 foo 中的最后一行,而是将其删除并依赖静态代码分析。例如,IntelliJ IDEA 具有“缺少 case 的 Enum switch 语句”代码检查,如果 foo 方法缺少 case<,则会对 foo 方法产生警告。 /代码>。

Rather than using some radical bytecode manipulation to enable a test to hit the last line in foo, I would remove it and rely on static code analysis instead. For example, IntelliJ IDEA has the "Enum switch statement that misses case" code inspection, which would produce a warning for the foo method if it lacked a case.

妄断弥空 2024-10-29 02:32:49

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

As you indicated in your edit, you can add the functionaliy in the enum itself. However, this might not be the best option, since it can violate the "One Responsibility" principle. Another way to achieve this is to create a static map which contains enum values as key and the functionality as value. This way, you can easily test if for any enum value you have a valid behavior by looping over all the values. It might be a bit far fetched on this example, but this is a technique I use often to map resource ids to enum values.

无需解释 2024-10-29 02:32:49

jMock(至少从我使用的 2.5.1 版本开始)可以开箱即用地执行此操作。您需要将 Mockery 设置为使用 ClassImposterizer。

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

jMock (at least as of version 2.5.1 that I'm using) can do this out of the box. You will need to set your Mockery to use ClassImposterizer.

Mockery mockery = new Mockery();
mockery.setImposterizer(ClassImposterizer.INSTANCE);
MyEnum unexpectedValue = mockery.mock(MyEnum.class);
一紙繁鸢 2024-10-29 02:32:49

首先,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

First of all Mockito can create mock data which can be integer long etc
It cannot create right enum as enum has specific number of ordinal name
value etc so if i have an enum

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

so i have total 5 ordinal in enum HttpMethod but mockito does not know it .Mockito creates mock data and its null all the time and you will end up in passing a null value .
So here is proposed solution that you randomize the ordinal and get a right enum which can be passed for other test

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

Output :

0
GET
0
GET
5
PATCH
5
PATCH
4
HEAD
5
PATCH
3
DELETE
0
GET
4
HEAD
2
PUT
说好的呢 2024-10-29 02:32:49

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

I think that the simplest way to reach the IllegalArgumentException is to pass null to the foo method and you will read "Do not know how to handle null"

看透却不说透 2024-10-29 02:32:49

我向我的枚举添加了一个 Unknown 选项,这是我在测试期间传入的。并非在所有情况下都理想,但很简单。

I added an Unknown option to my enum, which I pass in during the test. Not ideal in every case, but simple.

凤舞天涯 2024-10-29 02:32:49

我会将默认情况与枚举情况之一放在一起:

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

I would put the default case with one of enum cases:

  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();
    }
  }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文