Подтвердить что ты не робот

Извините 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 может быть достигнута какая-то черная магия.

Спасибо!

изменить

Поскольку у меня есть перечисление, я подумал, что могу просто добавить метод к значениям и, таким образом, полностью исключить проблему переключения; но я оставляю вопрос, поскольку он все еще интересен.

4b9b3361

Ответ 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();
    }
  }