Как смоделировать/подделать новое значение перечисления с помощью JMockit

Как добавить поддельное значение перечисления с помощью JMockit?

В документации ничего не нашел. Это вообще возможно?

Связанный: этот вопрос, но он предназначен только для mockito, а не для JMockIt.

РЕДАКТИРОВАТЬ: я удалил примеры, которые я дал в первую очередь, потому что примеры кажутся отвлекающими. Пожалуйста, взгляните на ответ с наибольшим количеством голосов на связанный вопрос, чтобы узнать, чего я ожидаю. Я хочу знать, можно ли сделать то же самое с JMockit.


person dyesdyes    schedule 24.08.2016    source источник
comment
Возможный дубликат Mocking Java enum для добавления значения чтобы проверить случай сбоя   -  person dorukayhan    schedule 24.08.2016
comment
@dorukayhan Дубликат? Я уже явно связал повторяющийся вопрос и сказал, что он не отвечает на этот вопрос, поскольку использует Mockito, а не JMockit. Не уверен, что еще я могу сказать, чтобы показать, что это не дубликат.   -  person dyesdyes    schedule 24.08.2016
comment
Подождите, это моя ошибка - я не видел часть The link is only for mockito. Прости.   -  person dorukayhan    schedule 24.08.2016
comment
Ваш дополнительный случай не совсем ясен: здесь вы не создаете исключение, и карта не должна содержать значения для всех записей перечисления, поэтому нет прямого отношения к какому-либо дополнительному значению перечисления или соответствующему тесту. Что вы на самом деле задаете, кроме вопроса, на который вы ссылаетесь?   -  person Oleg Sklyar    schedule 25.08.2016
comment
@OlegSklyar, на самом деле это тот же случай, но это показывает, что я хочу добавить новое значение перечисления, а не передавать переключатель, поскольку я тоже мог бы использовать карту. И да, карта может содержать не все элементы, но добавление поддельного элемента гарантирует, что я попаду в строку //доступа сюда.   -  person dyesdyes    schedule 25.08.2016


Ответы (3)


Я думаю, вы пытаетесь решить не ту проблему. Вместо этого исправьте метод foo(MyEnum) следующим образом:

    public int foo(MyEnum value) {
        switch (value) {
            case A: return 1;
            case B: return 2;
        }
        return 0; // just to satisfy the compiler
    }

Наличие throw в конце для захвата несуществующего воображаемого элемента перечисления бесполезно, так как он никогда не будет достигнут. Если вас беспокоит, что в перечисление добавляется новый элемент, а метод foo не обновляется соответствующим образом, есть лучшие решения. Один из них — полагаться на проверку кода из вашей Java IDE (IntelliJ по крайней мере имеет одну для этого случая: «оператор переключения для перечисляемого типа пропускает регистр») или правило из инструмента статического анализа.

Лучшее решение, однако, состоит в том, чтобы поместить постоянные значения, связанные с элементами перечисления, там, где они принадлежат (само перечисление), тем самым устраняя необходимость в switch:

public enum BetterEnum {
    A(1), B(2);

    public final int value;
    BetterEnum(int value) { this.value = value; }
}
person Rogério    schedule 24.08.2016
comment
Чем лучше полагаться на проверки кода, чем предотвращать возврат неверного значения, если кто-то еще изменяет структуру, которую не следовало изменять, без изменения кода ниже по течению? Этот кто-то даже не будет знать, что он должен был проверить проверки кода... - person Oleg Sklyar; 25.08.2016
comment
Вы пытаетесь решить первый пример, который я дал, а не вопрос, и я не согласен с вашим ответом и согласен с комментарием @OlegSklyar. Это потому, что люди не всегда запускают проверку кода, и мне нужен способ корректного модульного тестирования. Я не хочу полагаться на инструмент проверки кода для предотвращения ошибок. - person dyesdyes; 25.08.2016
comment
Видя, что вы создатель JMockit, я думаю, это означает, что это невозможно. Можете ли вы изменить свой ответ, чтобы сообщить мне, возможно ли это с помощью JMockit? Считаете ли вы это хорошей идеей или нет. - person dyesdyes; 25.08.2016
comment
@OlegSklyar Вы не замечаете, что тест, который хочет ОП, также не решит проблему. Даже если этот тест написан, когда кто-то добавляет элемент C в перечисление, нет гарантии, что он добавит его в метод с switch. Они либо получат исключение при вызове foo(C), либо недопустимое возвращаемое значение. Эта деталь не важна для моего ответа - дело в том, что тест не представляет ценности. - person Rogério; 25.08.2016
comment
Кроме того, обратите внимание, что лучшее решение, которое я рекомендовал, действительно решает проблему, поскольку оно потребует, чтобы любой новый элемент, добавляемый в перечисление, получал связанное с ним значение. И это не зависит от проверки кода. - person Rogério; 25.08.2016

Поразмыслив над проблемой, я нашел решение, и на удивление очень тривиальное.

Вы спрашиваете об издевательстве над перечислением или расширении его в тесте. Но реальная проблема, по-видимому, заключается в необходимости гарантировать, что любое расширение перечисления должно сопровождаться изменениями в функции, которая его использует. Таким образом, вам, по сути, нужен тест, который потерпит неудачу, если перечисление будет расширено, независимо от того, используется ли макет или вообще возможно. На самом деле лучше обойтись без, если это возможно.

У меня была точно такая же проблема несколько раз, но фактическое решение пришло мне в голову только сейчас, когда я увидел ваш вопрос:

Исходное перечисление:

public enum MyEnum { A, B }

Функция, которая была определена, когда перечисление предоставило только A и B:

public int mapper(MyEnum e) {
  switch (e) {
    case A: return 1;
    case B: return 2;
    default:
      throw new IllegalArgumentException("value not supported");
  }
}

Тест, который укажет, что mapper нужно будет обрабатывать при расширении перечисления:

@Test
public void test_mapper_onAllDefinedArgValues_success() {
  for (MyEnum e: MyEnum.values()) {
    mapper(e);
  }
}

Результат теста:

Process finished with exit code 0

Теперь давайте расширим перечисление новым значением C и перезапустим тест:

java.lang.IllegalArgumentException: value not supported

at io.ventu.rpc.amqp.AmqpResponderTest.mapper(AmqpResponderTest.java:104)
...
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Process finished with exit code 255
person Oleg Sklyar    schedule 25.08.2016
comment
Это проверит, что все значения перечисления обрабатываются, но не то, что потенциально добавленные значения перечисления вызовут исключение. А также это не приведет вас к 100% охвату отделений или даже 100% охвату линий. Если перечисление, например, исходит из внешнего кода, тест может не запуститься, когда перечисление расширено, поэтому это не подходящий полезный ответ. - person Vampire; 05.09.2019

Простого создания фальшивого значения перечисления, вероятно, будет недостаточно, в конечном итоге вам также потребуется манипулировать целочисленным массивом, созданным компилятором.


На самом деле, чтобы создать поддельное значение перечисления, вам даже не нужна какая-либо насмешливая структура. Вы можете просто использовать Objenesis для создания нового экземпляра класса перечисления (да, это работает), а затем использовать старое простое отражение Java для установки приватных полей name и ordinal, и у вас уже есть новый экземпляр перечисления.

Используя платформу Spock для тестирования, это будет выглядеть примерно так:

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, например. г. требуется 100% покрытие для случая default, который выдает исключение в случае расширения перечисления, все становится немного сложнее и в то же время немного проще.

Немного сложнее, потому что вам нужно серьезно подумать, чтобы манипулировать синтетическим полем, которое компилятор генерирует в синтетическом анонимном внутреннем классе, который генерирует компилятор, поэтому на самом деле не очевидно, что вы делаете, и вы привязаны к фактической реализации компилятора, так что это может сломаться в любое время в любой версии Java или даже если вы используете разные компиляторы для одной и той же версии Java. На самом деле это уже отличается между Java 6 и Java 8.

Немного проще, потому что вы можете забыть первые две части этого ответа, потому что вам вообще не нужно создавать новый экземпляр перечисления, вам просто нужно манипулировать 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 в какое-то значение, большее, чем массив, сгенерированный компилятором, либо вы получите одно из других значений switch-case, если нет , в обоих случаях это не помогло бы проверить разыскиваемое дело default.

Теперь вы можете получить это поле int[] и исправить его, чтобы оно содержало сопоставление исходного числа вашего экземпляра перечисления NON_EXISTENT. Но, как я сказал ранее, именно для этого варианта использования, тестирования случая default, вам вообще не нужны первые два шага. Вместо этого вы можете просто передать любой из существующих экземпляров enum тестируемому коду и просто манипулировать сопоставлением int[], чтобы сработал случай default.

Таким образом, все, что необходимо для этого тестового примера, на самом деле это, снова написанное в коде Spock (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.

Вот что делает этот код, который я только что показал:

  • он перебирает анонимные классы внутри класса, содержащего выражение переключения
  • в тех ищет поле с картой переключения
  • если поле не найдено, пробуется следующий класс
  • если Class.forName выдает ClassNotFoundException, тест завершается неудачей, что и предполагалось, потому что это означает, что вы скомпилировали код с помощью компилятора, который следует другой стратегии или шаблону именования, поэтому вам нужно добавить больше интеллекта, чтобы охватить разные стратегии компилятора для включение значений enum. Потому что, если класс с полем найден, break выходит из цикла for, и тест может продолжаться. Вся эта стратегия, конечно, зависит от нумерации анонимных классов, начиная с 1 и без пробелов, но я надеюсь, что это довольно безопасное предположение. Если вы имеете дело с компилятором, в котором это не так, алгоритм поиска необходимо соответствующим образом адаптировать.
  • если поле карты переключения найдено, создается новый массив int того же размера
  • новый массив заполнен Integer.MAX_VALUE, который обычно должен запускать случай default, если у вас нет перечисления с 2 147 483 647 значений
  • новый массив назначается полю карты переключателей
  • цикл for остается с использованием break
  • теперь можно выполнить фактический тест, запустив выражение switch для оценки
  • наконец (в блоке finally, если вы не используете Spock, в блоке cleanup, если вы используете Spock), чтобы убедиться, что это не повлияет на другие тесты того же класса, исходная карта переключателей помещается обратно в поле карты переключателей.
person Vampire    schedule 06.09.2019