Извините Java enum, чтобы добавить значение для проверки ошибки
У меня есть переключатель перечисления более или менее следующим образом:
public static enum MyEnum {A, B}
public int foo(MyEnum value) {
switch(value) {
case(A): return calculateSomething();
case(B): return calculateSomethingElse();
}
throw new IllegalArgumentException("Do not know how to handle " + value);
}
и я бы хотел, чтобы все линии были охвачены тестами, но поскольку код, как ожидается, будет иметь дело со всеми возможностями, я не могу предоставить значение без соответствующего оператора case в коммутаторе.
Расширение перечисления для добавления дополнительного значения невозможно, и просто высмеивать метод equals для возврата false
не будет работать либо потому, что генерируемый байт-код использует таблицу перехода за занавесками, чтобы перейти к соответствующему случаю. Итак, я подумал, что с PowerMock может быть достигнута какая-то черная магия.
Спасибо!
изменить
Поскольку у меня есть перечисление, я подумал, что могу просто добавить метод к значениям и, таким образом, полностью исключить проблему переключения; но я оставляю вопрос, поскольку он все еще интересен.
Ответы
Ответ 1
Вот полный пример.
Код почти как ваш оригинал (просто упрощенная проверка правильности теста):
public enum MyEnum {A, B}
public class Bar {
public int foo(MyEnum value) {
switch (value) {
case A: return 1;
case B: return 2;
}
throw new IllegalArgumentException("Do not know how to handle " + value);
}
}
И вот unit test с полным охватом кода, тест работает с Powermock (1.4.10), Mockito (1.8.5) и JUnit (4.8.2):
@RunWith(PowerMockRunner.class)
public class BarTest {
private Bar bar;
@Before
public void createBar() {
bar = new Bar();
}
@Test(expected = IllegalArgumentException.class)
@PrepareForTest(MyEnum.class)
public void unknownValueShouldThrowException() throws Exception {
MyEnum C = PowerMockito.mock(MyEnum.class);
Whitebox.setInternalState(C, "name", "C");
Whitebox.setInternalState(C, "ordinal", 2);
PowerMockito.mockStatic(MyEnum.class);
PowerMockito.when(MyEnum.values()).thenReturn(new MyEnum[]{MyEnum.A, MyEnum.B, C});
bar.foo(C);
}
@Test
public void AShouldReturn1() {
assertEquals(1, bar.foo(MyEnum.A));
}
@Test
public void BShouldReturn2() {
assertEquals(2, bar.foo(MyEnum.B));
}
}
Результат:
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.628 sec
Ответ 2
@Melloware
... код, который выполняет оператор switch(), java вызывает java.lang.ArrayIndexOutOfBounds...
У меня такая же проблема. Запустите тест с новым Enum, как первый в своем тестовом классе. Я создал ошибку с этой проблемой: https://code.google.com/p/powermock/issues/detail?id=440
Ответ 3
Вместо того, чтобы использовать некоторые радикальные манипуляции байт-кодами, чтобы включить тест, чтобы попасть в последнюю строку в foo
, я бы удалил его и вместо этого полагался на статический анализ кода. Например, IntelliJ IDEA имеет оператор Enum switch
, который пропускает проверку кода случая, что приведет к предупреждению для метода foo
, если ему не хватает case
.
Ответ 4
Как вы указали в своем редактировании, вы можете добавить функционал в самом перечислении. Однако это может быть не лучшим вариантом, поскольку это может нарушить принцип "Одна ответственность". Другой способ добиться этого - создать статическую карту, которая содержит значения перечисления в качестве ключа и функциональность как значение. Таким образом, вы можете легко проверить, если для любого значения перечисления у вас есть допустимое поведение, перебирая все значения. В этом примере это может быть немного странно, но это метод, который я часто использую для сопоставления идентификаторов ресурсов с значениями перечисления.
Ответ 5
jMock (по крайней мере, с версии 2.5.1, которую я использую) может сделать это из коробки. Вам нужно будет настроить свой Mockery для использования ClassImposterizer.
Mockery mockery = new Mockery();
mockery.setImposterizer(ClassImposterizer.INSTANCE);
MyEnum unexpectedValue = mockery.mock(MyEnum.class);
Ответ 6
Прежде всего, Mockito может создавать макетные данные, которые могут быть целыми и длинными и т.д.
Он не может создать правое перечисление, поскольку перечисление имеет определенное количество порядковых имен
значение и т.д., поэтому, если у меня есть перечисление
public enum HttpMethod {
GET, POST, PUT, DELETE, HEAD, PATCH;
}
так что у меня есть всего 5 порядковых номеров в перечислении HttpMethod, но mockito этого не знает .Mockito создает ложные данные и их нулевые все время, и вы закончите с пропуском нулевого значения.
Итак, здесь предлагается решение, которое вы производите по порядку и получаете правое перечисление, которое может быть передано для другого теста
import static org.mockito.Mockito.mock;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.internal.util.reflection.Whitebox;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import com.amazonaws.HttpMethod;
//@Test(expected = {"LoadableBuilderTestGroup"})
//@RunWith(PowerMockRunner.class)
public class testjava {
// private static final Class HttpMethod.getClass() = null;
private HttpMethod mockEnumerable;
@Test
public void setUpallpossible_value_of_enum () {
for ( int i=0 ;i<10;i++){
String name;
mockEnumerable= Matchers.any(HttpMethod.class);
if(mockEnumerable!= null){
System.out.println(mockEnumerable.ordinal());
System.out.println(mockEnumerable.name());
System.out.println(mockEnumerable.name()+"mocking suceess");
}
else {
//Randomize all possible value of enum
Random rand = new Random();
int ordinal = rand.nextInt(HttpMethod.values().length);
// 0-9. mockEnumerable=
mockEnumerable= HttpMethod.values()[ordinal];
System.out.println(mockEnumerable.ordinal());
System.out.println(mockEnumerable.name());
}
}
}
@Test
public void setUpallpossible_value_of_enumwithintany () {
for ( int i=0 ;i<10;i++){
String name;
mockEnumerable= Matchers.any(HttpMethod.class);
if(mockEnumerable!= null){
System.out.println(mockEnumerable.ordinal());
System.out.println(mockEnumerable.name());
System.out.println(mockEnumerable.name()+"mocking suceess");
} else {
int ordinal;
//Randomize all possible value of enum
Random rand = new Random();
int imatch = Matchers.anyInt();
if( imatch>HttpMethod.values().length)
ordinal = 0 ;
else
ordinal = rand.nextInt(HttpMethod.values().length);
// 0-9. mockEnumerable=
mockEnumerable= HttpMethod.values()[ordinal];
System.out.println(mockEnumerable.ordinal());
System.out.println(mockEnumerable.name());
}
}
}
}
Выход:
0
GET
0
GET
5
PATCH
5
PATCH
4
HEAD
5
PATCH
3
DELETE
0
GET
4
HEAD
2
PUT
Ответ 7
Я думаю, что самый простой способ достичь IllegalArgumentException - передать null методу foo, и вы прочтете "Не знаю, как обрабатывать нуль"
Ответ 8
Недостаточно просто создать ложное значение перечисления, в конечном итоге вам также потребуется манипулировать целочисленным массивом, который создается компилятором.
На самом деле, чтобы создать поддельное значение enum, вам даже не нужны какие-либо насмешливые рамки. Вы можете просто использовать Objenesis для создания нового экземпляра класса enum (да, это работает), а затем использовать простое старое отражение Java для установки закрытых полей name
и ordinal
, и у вас уже есть новый экземпляр enum.
Используя Spock Framework для тестирования, это будет выглядеть примерно так:
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())
}
Если вы также хотите, чтобы метод MyEnum.values()
возвращал новое перечисление, теперь вы можете использовать JMockit для имитации вызова values()
, например,
new MockUp<MyEnum>() {
@Mock
MyEnum[] values() {
[*originalEnumValues, NON_EXISTENT] as MyEnum[]
}
}
или вы можете снова использовать обычное старое отражение для манипулирования полем $VALUES
, например:
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)
}
Пока вы не имеете дело с выражением switch
, но с некоторым if
или подобным, вам может быть достаточно либо первой части, либо первой и второй части.
Если вы, однако, имеете дело с выражением switch
, e. грамм. желая 100% покрытия для случая default
, который выдает исключение в случае расширения enum, как в вашем примере, все становится немного сложнее и в то же время немного проще.
Немного сложнее, потому что вам нужно серьезно подумать, чтобы манипулировать синтетическим полем, которое генерирует компилятор в синтетическом анонимном внутреннем классе, который генерирует компилятор, поэтому не совсем очевидно, что вы делаете, и вы привязаны к реальной реализации компилятора, так что это может сломаться в любое время в любой версии Java или даже если вы используете разные компиляторы для одной и той же версии Java. Это на самом деле уже отличается между Java 6 и Java 8.
Немного проще, потому что вы можете забыть первые две части этого ответа, потому что вам вообще не нужно создавать новый экземпляр enum, вам просто нужно манипулировать int[]
, которым вам нужно все равно манипулировать, чтобы сделать тест, который вы хотите.
Недавно я нашел очень хорошую статью об этом на https://www.javaspecialists.eu/archive/Issue161.html.
Большая часть информации все еще действительна, за исключением того, что теперь внутренний класс, содержащий карту переключателей, больше не является именованным внутренним классом, а анонимным классом, поэтому вы больше не можете использовать getDeclaredClasses
, а должны использовать другой подход, показанный ниже.
Подводя итог, можно сказать, что включение уровня байт-кода не работает с перечислениями, а только с целыми числами. Итак, что делает компилятор, он создает анонимный внутренний класс (ранее именованный внутренний класс согласно написанию статьи, это Java 6 против Java 8), который содержит одно статическое конечное поле int[]
с именем $SwitchMap$net$kautler$MyEnum
, которое заполняется с целыми числами 1, 2, 3,... в индексах значений MyEnum#ordinal()
.
Это означает, что когда код приходит к фактическому переключателю, он делает
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);
}
Если сейчас myEnumVariable
будет иметь значение NON_EXISTENT
, созданное на первом шаге выше, вы либо получите ArrayIndexOutOfBoundsException
, если вы установите ordinal
на какое-то значение, большее, чем массив, сгенерированный компилятором, или вы бы получить одно из других значений регистра переключателя, если нет, в обоих случаях это не поможет проверить требуемый случай default
.
Теперь вы можете получить это поле int[]
и исправить его, чтобы оно содержало отображение для ординала вашего экземпляра перечисления NON_EXISTENT
. Но, как я уже говорил ранее, именно для этого варианта использования, тестирующего вариант default
, вам не нужны первые два шага вообще. Вместо этого вы можете просто передать любой из существующих экземпляров enum тестируемому коду и просто манипулировать отображением int[]
, чтобы вызвать случай default
.
Таким образом, все, что необходимо для этого тестового примера, на самом деле это, опять же написанный в коде Спока (Groovy), но вы можете легко адаптировать его и к Java:
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)
В этом случае вам не нужны какие-либо насмешливые рамки. На самом деле это не помогло бы вам в любом случае, поскольку ни одна из насмешливых рамок, о которых я знаю, не позволяла бы имитировать доступ к массиву. Вы можете использовать JMockit или любую среду для пересмешки, чтобы высмеивать возвращаемое значение ordinal()
, но это опять-таки приведет к другой ветки переключателя или AIOOBE.
Этот код, который я только что показал:
- он проходит по анонимным классам внутри класса, который содержит выражение переключателя
- в тех, которые ищет поле с картой переключателей
- если поле не найдено, пробуется следующий класс
- если
ClassNotFoundException
выбрасывается Class.forName
, тест завершается неудачно, что подразумевается, потому что это означает, что вы скомпилировали код с помощью компилятора, который следует другой стратегии или шаблону именования, поэтому вам нужно добавить еще немного интеллекта, чтобы покрыть различные стратегии компилятора для включения значений enum. Потому что, если класс с полем найден, break
покидает цикл for, и тест можно продолжить. Конечно, вся эта стратегия зависит от нумерации анонимных классов, начиная с 1 и без пробелов, но я надеюсь, что это довольно безопасное предположение. Если вы имеете дело с компилятором, где это не так, алгоритм поиска необходимо соответствующим образом адаптировать.
- если поле карты переключения найдено, создается новый массив типа int такого же размера
- новый массив заполнен
Integer.MAX_VALUE
, который обычно должен запускать регистр default
, если у вас нет перечисления со значениями 2 147 483 647
- новый массив назначен полю карты переключения
- цикл for оставлен с помощью
break
- теперь можно выполнить реальный тест, запустив выражение switch для оценки
- наконец (в блоке
finally
, если вы не используете Spock, в блоке cleanup
, если вы используете Spock), чтобы убедиться, что это не влияет на другие тесты того же класса, исходная карта переключателей помещается обратно в переключить поле карты
Ответ 9
Я бы поставил случай по умолчанию с одним из случаев перечисления:
public static enum MyEnum {A, B}
public int foo(MyEnum value) {
if (value == null) throw new IllegalArgumentException("Do not know how to handle " + value);
switch(value) {
case(A):
return calculateSomething();
case(B):
default:
return calculateSomethingElse();
}
}