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 хорошо его понимает.