Core_F
Core_F

Reputation: 3442

Unit Test for Enum value that doesn't exist?

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?

Upvotes: 19

Views: 25760

Answers (5)

clstrfsck
clstrfsck

Reputation: 14829

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

Upvotes: 6

Damian
Damian

Reputation: 550

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

Upvotes: 2

Swapnil
Swapnil

Reputation: 1134

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


    }
}

Upvotes: 1

Vinay Veluri
Vinay Veluri

Reputation: 6855

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

Upvotes: 4

assylias
assylias

Reputation: 328598

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)

Upvotes: 8

Related Questions