18

Some example code first...

The enum:

public enum TestEnum {
   YES,
   NO
}

Some code:

public static boolean WorkTheEnum(TestEnum theEnum) {
   switch (theEnum) {
      case YES:
         return true;
      case NO:
         return false;
      default:
         // throws an exception here
   }
}

Problem:
The TestEnum is something I import from a different code of a different developer. So it actually could change. For this case I want to have a unit test that actually checks for that non existing value. But I simply don't know how to do it with Mockito and JUnit.

This part is of course not working:

@Test(expected=Exception.class)
public void DoesNotExist_throwsException() throws Exception {
    when(TestEnum.MAYBE).thenReturn(TestEnum.MAYBE);
    WorkTheEnum(TestEnum.MAYBE);
}

I found one example that usees PowerMock, but I couldn't get it to work with Mockito.

Any ideas?

Core_F
  • 3,372
  • 3
  • 30
  • 53
  • I'm not confident enough in claiming this to put it as an answer, but: Is it not possible to do `TestEnum notAValidEnum = Mockito.mock(TestEnum.class);`, and then feed it to `WorkTheEnum()`...? – kinbiko Dec 18 '14 at 21:32
  • Possible duplicate: https://stackoverflow.com/questions/5323505 – augurar Jan 10 '18 at 20:15
  • Possibly related: https://stackoverflow.com/questions/28013717/eclemma-branch-coverage-for-switch-7-of-19-missed/28015212#28015212 – nrainer May 10 '19 at 14:20

5 Answers5

8

How about a simple:

Set<String> expected = new HashSet<> (Arrays.asList("YES", "NO"));
Set<String> actual = new HashSet<>();
for (TestEnum e : TestEnum.values()) actual.add(e.name());
assertEquals(expected, actual);

(using HashSet rather than ArrayList because order does not matter)

assylias
  • 321,522
  • 82
  • 660
  • 783
  • 1
    But that would only test the content of the enum and not the default path of the switch statement. – Core_F Sep 19 '14 at 08:41
  • I see - you would like to make sure that the method throws an exception if the default case is reached so you would like to mock the enum to simulate a non-expected value. Is that right? – assylias Sep 19 '14 at 08:48
  • That's exactly what I want to do, yes. – Core_F Sep 19 '14 at 08:56
  • @Feroc the problem is that [you can't instantiate a new instance of an enum via reflection](http://stackoverflow.com/questions/9614282/how-to-create-an-instance-of-enum-using-reflection-in-java) so I'm not sure if mockito can work around that limitation... – assylias Sep 19 '14 at 09:21
6

Building on the answer from @assylias, I think this is the best you can do:

List<String> unknown = new ArrayList<>();
for (TestEnum e : TestEnum.values())
  unknown.add(e.name());
unknown.removeAll(Arrays.asList("YES", "NO"));
if (unknown.isEmpty()) {
  // Not possible to reach default case, do whatever you need to do
} else {
  TestEnum notIncluded = TestEnum.valueOf(unknown.get(0));
  workTheEnum(notIncluded);
}

It isn't possible (AFAIK) to fake a non-existent enum value in a switch statement, due to the way that enum switch statements are compiled. Even if you resort to fiddling with the internal ordinal field in the enum instance via reflection, the switch statement will give an ArrayIndexOutOfBoundsException rather than falling through to the default case.


Here is some code that looks like it might work, but doesn't, due to the ArrayIndexOutOfBoundsException mentioned above:

TestEnum abused = TestEnum.YES;
try {
  Class<?> c = abused.getClass().getSuperclass();
  Field[] declaredFields = c.getDeclaredFields();
  Field ordinalField = null;
  for (Field e : declaredFields) {
    if (e.getName().equals("ordinal")) {
      ordinalField = e;
    }
  }
  ordinalField.setAccessible(true);
  ordinalField.setInt(abused, TestEnum.values().length);
  workTheEnum(abused);
} catch (Exception e) {
  e.printStackTrace(System.err);
}

OK, here is something that might work for you. It's pretty hacky, so to me it's probably worse than not having 100% code coverage, YMMV. It works by replacing the enum ordinal lookup arrays with arrays containing all zeros, which falls through to the default case.

// Setup values - needs to be called so that
// $SWITCH_TABLE$FooClass$BarEnum is initialised.
workTheEnum(TestEnum.YES);
workTheEnum(TestEnum.NO);

// This is the class with the switch statement in it.
Class<?> c = ClassWithSwitchStatement.class;

// Find and change fields.
Map<Field, int[]> changedFields = new HashMap<>();
Field[] declaredFields = c.getDeclaredFields();
try {
  for (Field f : declaredFields) {
    if (f.getName().startsWith("$SWITCH_TABLE$")) {
      f.setAccessible(true);
      int[] table = (int[])f.get(null);
      f.set(null, new int[table.length]);
      changedFields.put(f, table);
    }
  }
  workTheEnum(TestEnum.YES);
} finally {
  for (Map.Entry<Field, int[]> entry : changedFields.entrySet()) {
    try {
      entry.getKey().set(null, entry.getValue());
    } catch (Exception ex) {
      ex.printStackTrace(System.err);
    }
  }
}
clstrfsck
  • 14,715
  • 4
  • 44
  • 59
  • Maybe mocking just isn't the correct way? But there has to be a way to use reflection to add a value so that the enum is actually valid and has all those values!? – Core_F Sep 19 '14 at 08:58
  • 1
    No, I don't think so. When the switch statement is compiled, there is an array generated with all the ordinal numbers of the enum values that exist at that point in time. If you try to modify existing values to change their ordinal numbers, the compiler generated code will access values outside the bounds of the array and fail with `ArrayIndexOutOfBoundsException`. I'll attach some code that does this so you can see if you like. – clstrfsck Sep 19 '14 at 09:13
  • Thanks for the detailed answer @msandiford. Bad luck I guess. – Core_F Sep 19 '14 at 10:04
  • For me it only works when adding `f.setAccessible(true);` to the loop (see my edit). This is probably related to Java8? – sjngm Jul 11 '17 at 12:16
  • Another note: for me it doesn't work with Maven (-> exclude it there). Once Maven compiled the class with the switch, it's necessary to compile that class in Eclipse again to make the test work again in Eclipse. In other words the compiler plays an important role whether or not the test works. – sjngm Jul 14 '17 at 08:11
4

Mockito doesn't support mocking of enum values but powermock does.

Try this.

I have created my own classes to simulate them. Please map to your own classes.

@RunWith(PowerMockRunner.class)
@PrepareForTest(Trail.class)
public class TrailTest {
    @Before
    public void setUp() {
        Trail mockTrail = PowerMock.createMock(Trail.class);
        Whitebox.setInternalState(mockTrail, "name", "Default");
        Whitebox.setInternalState(mockTrail, "ordinal", 2);
        PowerMock.mockStatic(Trail.class);
        expect(Trail.values()).andReturn(new Trail[]{Trail.YES, Trail.NO, mockTrail});
        expect(Trail.valueOf("default value")).andReturn(mockTrail);
        PowerMock.replay(Trail.class);
    }

    @Test(expected = RuntimeException.class)
    public void test() {
        Trail aDefault = Trail.valueOf("default value");
        BasicTrails.find(aDefault);
    }
}

This is the method :

public class BasicTrails {

public static boolean find(Trail trail) {
    switch (trail) {
        case YES:
            return true;
        case NO:
            return false;
        default:
            throw new RuntimeException("Invalid");
    }
}

This is the enum

public enum Trail {
    YES, NO;
}
Vinay Veluri
  • 6,671
  • 5
  • 32
  • 56
  • Thanks for your help. Not really sure if we can use Powermock here. – Core_F Sep 19 '14 at 10:03
  • Its just another `mock` library. That depends on choice anyhow. – Vinay Veluri Sep 19 '14 at 10:10
  • you can improve answer if convert your code accordinly the OP question – gstackoverflow Mar 06 '15 at 10:38
  • 2
    @VinayVeluri: PowerMock is not just another mock library. It's more powerful than Mockito and this is why some people do not want to use it in their projects. It's rather used to test legacy code, e.g. static methods which can't be tested by "normal" mock libraries like Mockito. As long as you develop a new project, then you should develop it in such way that you won't need PowerMock, but Mockito will be enough. – krm Dec 07 '15 at 13:34
1

With the help of Powermock we can achieve this as Powermock supports mocking of final classes

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.BDDMockito;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest(Trail.class)

public class TrailTest {

    @Mock Trail mockTrail;

    @Before
    public void setUp() {
        PowerMockito.mockStatic(Trail.class);
        BDDMockito.given(Trail.values()).willReturn(new Trail[]{Trail.YES, Trail.NO, mockTrail});
        BDDMockito.given(Trail.valueOf("YES")).willReturn(mockTrail.YES);
        BDDMockito.given(Trail.valueOf("NO")).willReturn(mockTrail.NO);

    }

    @Test
    public void test() {

        assertTrue(BasicTrails.find(mockTrail.valueOf("YES")));

        assertFalse(BasicTrails.find(mockTrail.valueOf("NO")));

        try{
             Trail aDefault = mockTrail.valueOf("default value");
        }catch (Exception e) {
            System.out.println(e);
        }


    }
}
Swapnil
  • 1,004
  • 1
  • 13
  • 21
1

You can mock, hack or trying to make it work but there is quite simple way how to do this. I assume that you are working with maven or gradle so you have main and test profiles.

Then in main profile you have code as above:

package my.cool.package;

public enum TestEnum {
    YES,
    NO
}

but then in test profile you can have another one:

// EXACTLY SAME as above
package my.cool.package;

public enum TestEnum {
    YES,
    NO,
    INVALID_FOR_TEST_ONLY
}

and now you can use new value INVALID_FOR_TEST_ONLY in test and it won't be available in prod profile.

There are two disadvantages:

  • if you update prod enum you may need to update test as well (if you want test then to)
  • some IDE may not work with this trick properly even maven understands it well
Damian
  • 437
  • 5
  • 11