4

How can I add a fake enum value using JMockit?

I could't find anything in the documentation. Is it even possible?

Related: this question but it is for mockito only, not for JMockIt.

EDIT: I removed the examples I gave in the first place because the examples seem to be distracting. Please have a look at the most upvoted answer on the linked question to see what I'm expecting. I want to know if it's possible to do the same with JMockit.

Community
  • 1
  • 1
dyesdyes
  • 1,147
  • 3
  • 24
  • 39
  • Possible duplicate of [Mocking Java enum to add a value to test fail case](http://stackoverflow.com/questions/5323505/mocking-java-enum-to-add-a-value-to-test-fail-case) – The SE I loved is dead Aug 24 '16 at 06:24
  • @dorukayhan Duplicate? I already explicitly linked the "duplicate" question and said that it doesn't answer this question as it is using Mockito and not JMockit. Not sure what else I can say to show that it's not a duplicate. – dyesdyes Aug 24 '16 at 06:46
  • Wait, it's my bad - I didn't see the `The link is only for mockito` part. Sorry. – The SE I loved is dead Aug 24 '16 at 06:49
  • Your "additional" case is not really clear: you are not throwing an exception here and the map does not have to contain values for all enum entries, so there is no direct relation to any extra enum value or the corresponding test. What are you actually asking different from the question you refer to? – Oleg Sklyar Aug 24 '16 at 22:16
  • @OlegSklyar it is actually the same case, but that's to show that I want to add a new enum value, not by pass the switch as I could use a map too. And yes the map might not contain all the elements, but adding a fake element will ensure that I hit the "// access here" line. – dyesdyes Aug 24 '16 at 23:32

3 Answers3

1

I think you are trying to solve the wrong problem. Instead, fix the foo(MyEnum) method as follows:

    public int foo(MyEnum value) {
        switch (value) {
            case A: return 1;
            case B: return 2;
        }
        return 0; // just to satisfy the compiler
    }

Having a throw at the end to capture an imaginary enum element that doesn't exist is not useful, as it will never be reached. If you are concerned about a new element getting added to the enum and the foo method not being updated accordingly, there are better solutions. One of them is to rely on a code inspection from your Java IDE (IntelliJ at least has one for this case: "switch statement on enumerated type misses case") or a rule from a static analysis tool.

The best solution, though, is to put constant values associated to enum elements where they belong (the enum itself), therefore eliminating the need for a switch:

public enum BetterEnum {
    A(1), B(2);

    public final int value;
    BetterEnum(int value) { this.value = value; }
}
Rogério
  • 16,171
  • 2
  • 50
  • 63
  • How is relying on code inspections better than preventing an incorrect value returned if somebody else modifies the structure that shouldn't have been modified without changing code downstream? This somebody won't even know that he should have checked code inspections... – Oleg Sklyar Aug 24 '16 at 22:05
  • You are trying to solve the first example I gave, not the question and I disagree with your answer and agree with @OlegSklyar comment. That's because people don't always run the code inspection that I want a way to unit test correctly. I don't want to rely of a code inspection tool to prevent bugs. – dyesdyes Aug 24 '16 at 23:39
  • Seeing that you are the creator of JMockit, I guess it means it's not possible. Can you change your answer to let me know if it's possible, using JMockit? Whether you think it's a good idea or not. – dyesdyes Aug 24 '16 at 23:40
  • @OlegSklyar You fail to notice that the test the OP wants *would not solve the problem eitther*. Even if that test is written, when someone adds element "C" to the enum, there is no guarantee they will add it to the method with a `switch`. They will either get an exception from calling `foo(C)` or an invalid return value This detail is not essential to my answer - the point is the test is not valuable. – Rogério Aug 25 '16 at 16:34
  • Also, note the "best solution" I recommended does solve the problem, as it will require that any new element added to the enum gets its associated value. And it does not rely on any code inspection. – Rogério Aug 25 '16 at 16:36
0

Having had a second thought about the problem I have a solution, and surprisingly a really trivial one.

You are asking about mocking an enum or extending it in test. But the actual problem actually seems to be the necessity to guarantee that any enum extension must be accompanied by amendments in the function that uses it. So you essentially need a test that would fail if the enum is extended, no matter if a mock is used or at all possible. In fact it is better to go without if possible.

I had exactly the same problem a number of times, but the actual solution came to my mind just now after seeing your question:

The original enum:

public enum MyEnum { A, B }

The function that has been defined when the enum provided only A and B:

public int mapper(MyEnum e) {
  switch (e) {
    case A: return 1;
    case B: return 2;
    default:
      throw new IllegalArgumentException("value not supported");
  }
}

The test that will point out that mapper will need to be dealt with when the enum is extended:

@Test
public void test_mapper_onAllDefinedArgValues_success() {
  for (MyEnum e: MyEnum.values()) {
    mapper(e);
  }
}

The test result:

Process finished with exit code 0

Now let's extend the enum with a new value C and rerun the test:

java.lang.IllegalArgumentException: value not supported

at io.ventu.rpc.amqp.AmqpResponderTest.mapper(AmqpResponderTest.java:104)
...
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Process finished with exit code 255
Oleg Sklyar
  • 9,834
  • 6
  • 39
  • 62
  • That would test that all enum values are handled, but not that potentially added enum values will throw an exception. And also this will not bring you to 100% branch coverage or even 100% line coverage. If the enum for example comes from external code, the test might not run when the enum is extended, so this is not an appropriate helpful answer. – Vampire Sep 05 '19 at 12:33
0

Just creating a fake enum value will probably 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, 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
Vampire
  • 35,631
  • 4
  • 76
  • 102