Простого создания фальшивого значения перечисления, вероятно, будет недостаточно, в конечном итоге вам также потребуется манипулировать целочисленным массивом, созданным компилятором.
На самом деле, чтобы создать поддельное значение перечисления, вам даже не нужна какая-либо насмешливая структура. Вы можете просто использовать 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
The link is only for mockito
. Прости. - person dorukayhan   schedule 24.08.2016