Unit Test для значения Enum, которое не существует?

Сначала введите пример кода...

Перечисление:

public enum TestEnum {
   YES,
   NO
}

Некоторые коды:

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

Проблема:
TestEnum - это то, что я импортирую из другого кода другого разработчика. Так что это может измениться. Для этого случая я хочу иметь unit test, который фактически проверяет это несуществующее значение. Но я просто не знаю, как это сделать с Mockito и JUnit.

Эта часть, конечно, не работает:

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

Я нашел один пример, который использует PowerMock, но я не мог заставить его работать с Mockito.

Любые идеи?

Ответ 1

Как насчет простой:

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

(используя HashSet, а не ArrayList, потому что порядок не имеет значения)

Ответ 2

Основываясь на ответе от @assylias, я думаю, что это лучшее, что вы можете сделать:

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

Невозможно (AFAIK) подделать несуществующее значение enum в операторе switch из-за того, как скомпилированы инструкции enum switch. Даже если вы прибегаете к возиться с внутренним полем ordinal в экземпляре enum через отражение, оператор switch даст ArrayIndexOutOfBoundsException, а не попадает в случай default.


Вот какой код выглядит так, как будто он может работать, но не из-за упомянутого выше ArrayIndexOutOfBoundsException:

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

ОК, вот что-то, что может сработать для вас. Это довольно взломанно, поэтому для меня это, вероятно, хуже, чем отсутствие покрытия на 100% кода, YMMV. Он работает, заменяя массивы массивов enum порядковых запросов массивами, содержащими все нули, которые попадают в случай по умолчанию.

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

Ответ 3

Mockito не поддерживает насмешку над значениями перечисления, но powermock делает.

Попробуйте это.

Я создал свои собственные классы, чтобы имитировать их. Просьба сопоставить свои занятия.

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

Это метод:

public class BasicTrails {

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

Это перечисление

public enum Trail {
    YES, NO;
}

Ответ 4

С помощью Powermock мы можем добиться этого, поскольку Powermock поддерживает насмешку над финальными классами

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


    }
}

Ответ 5

Вы можете высмеивать, взламывать или пытаться заставить его работать, но есть простой способ, как это сделать. Я предполагаю, что вы работаете с maven или gradle, поэтому у вас есть профили main и test.

Затем в главном профиле у вас есть код, как указано выше:

package my.cool.package;

public enum TestEnum {
    YES,
    NO
}

но затем в тестовом профиле вы можете получить еще один:

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

public enum TestEnum {
    YES,
    NO,
    INVALID_FOR_TEST_ONLY
}

и теперь вы можете использовать новое значение INVALID_FOR_TEST_ONLY в test, и оно не будет доступно в профиле prod.

Есть два недостатка:

  • если вы обновите prod enum, вам может понадобиться также обновить тест (если вы хотите, чтобы затем тест)
  • некоторые IDE могут не работать с этим трюком должным образом, даже maven хорошо его понимает.