Mocking Java enum to add a value to test fail case

前端 未结 10 1406
死守一世寂寞
死守一世寂寞 2020-11-28 23:55

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


        
相关标签:
10条回答
  • 2020-11-29 00:51

    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();
        }
      }
    
    0 讨论(0)
  • 2020-11-29 00:52

    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
    
    0 讨论(0)
  • 2020-11-29 00:52

    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"

    0 讨论(0)
  • 2020-11-29 00:59

    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
    0 讨论(0)
提交回复
热议问题