Es reicht nicht aus, nur einen gefälschten Enum-Wert zu erstellen. Schließlich müssen Sie auch ein vom Compiler erstelltes Integer-Array bearbeiten.
Um einen gefälschten Enum-Wert zu erstellen, benötigen Sie nicht einmal ein spöttisches Framework. Sie können nur Objenesis verwenden , um eine neue Instanz der Enum - Klasse (ja, das funktioniert) und verwenden Sie dann Plain Old Java Reflexion auf die privaten Felder zu setzen name
und ordinal
und haben Sie bereits Ihre neue Enum - Instanz.
Wenn Sie das Spock-Framework zum Testen verwenden, sieht dies ungefähr so aus:
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())
}
Wenn die MyEnum.values()
Methode auch die neue Aufzählung zurückgeben soll, können Sie jetzt entweder JMockit verwenden, um den values()
Aufruf wie zu verspotten
new MockUp<MyEnum>() {
@Mock
MyEnum[] values() {
[*originalEnumValues, NON_EXISTENT] as MyEnum[]
}
}
oder Sie können wieder einfache alte Reflexion verwenden, um das $VALUES
Feld wie folgt zu manipulieren :
given:
getPrivateFinalFieldForSetting.curry(MyEnum).with {
it('$VALUES').set(null, [*originalEnumValues, NON_EXISTENT] as MyEnum[])
}
expect:
true
cleanup:
getPrivateFinalFieldForSetting.curry(MyEnum).with {
it('$VALUES').set(null, originalEnumValues)
}
Solange Sie sich nicht mit einem switch
Ausdruck befassen , sondern mit einigen if
s oder ähnlichem, kann entweder nur der erste Teil oder der erste und zweite Teil für Sie ausreichen.
Wenn Sie sich jedoch mit einem switch
Ausdruck befassen , z. B. eine 100% ige Abdeckung für den default
Fall, der eine Ausnahme auslöst, falls die Aufzählung wie in Ihrem Beispiel erweitert wird, werden die Dinge etwas komplizierter und gleichzeitig etwas einfacher.
Etwas komplizierter, da Sie ernsthafte Überlegungen anstellen müssen, um ein synthetisches Feld zu manipulieren, das der Compiler in einer synthetischen anonymen Innner-Klasse generiert, die der Compiler generiert. Daher ist es nicht wirklich offensichtlich, was Sie tun, und Sie sind an die tatsächliche Implementierung gebunden des Compilers, so dass dies in jeder Java-Version jederzeit oder sogar dann unterbrochen werden kann, wenn Sie unterschiedliche Compiler für dieselbe Java-Version verwenden. Es ist tatsächlich schon anders zwischen Java 6 und Java 8.
Ein bisschen einfacher, weil Sie die ersten beiden Teile dieser Antwort vergessen können, weil Sie überhaupt keine neue Enum-Instanz erstellen müssen, sondern nur eine manipulieren müssen int[]
, die Sie sowieso manipulieren müssen, um den Test für Sie durchzuführen wollen.
Ich habe kürzlich einen sehr guten Artikel dazu unter https://www.javaspecialists.eu/archive/Issue161.html gefunden .
Die meisten Informationen dort sind noch gültig, außer dass die innere Klasse, die die Switch-Map enthält, keine benannte innere Klasse mehr ist, sondern eine anonyme Klasse. Sie können sie also nicht getDeclaredClasses
mehr verwenden, sondern müssen einen anderen Ansatz verwenden, der unten gezeigt wird.
Grundsätzlich funktioniert das Einschalten der Bytecode-Ebene nicht mit Aufzählungen, sondern nur mit Ganzzahlen. Also , was der Compiler tut , ist es eine anonyme innere Klasse erstellt (vorher eine benannte innere Klasse gemäß dem Artikel zu schreiben, das ist Java 6 vs. Java 8) , die ein statisches endgültiges hält int[]
Feld genannt , $SwitchMap$net$kautler$MyEnum
die mit ganzen Zahlen 1, gefüllt 2, 3, ... an den Werteindizes MyEnum#ordinal()
.
Dies bedeutet, wenn der Code zum eigentlichen Schalter kommt, tut er dies
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);
}
Wenn jetzt myEnumVariable
der Wert NON_EXISTENT
im ersten Schritt oben erstellt werden würde, würden Sie entweder einen erhalten, ArrayIndexOutOfBoundsException
wenn Sie einen ordinal
Wert festlegen , der größer als das vom Compiler generierte Array ist, oder Sie würden in beiden Fällen einen der anderen Switch-Case-Werte erhalten, wenn dies nicht der Fall ist Dies würde nicht helfen, den gewünschten default
Fall zu testen .
Sie können dieses int[]
Feld jetzt abrufen und so reparieren, dass es eine Zuordnung für das Orinal Ihrer NON_EXISTENT
Enum-Instanz enthält. Aber wie ich bereits sagte, default
brauchen Sie für genau diesen Anwendungsfall, in dem Sie den Fall testen , die ersten beiden Schritte überhaupt nicht. Stattdessen können Sie dem zu testenden Code einfach eine der vorhandenen Enum-Instanzen zuweisen und die Zuordnung einfach int[]
so bearbeiten , dass der default
Fall ausgelöst wird.
Für diesen Testfall ist also nur Folgendes erforderlich, das wiederum in Spock (Groovy) -Code geschrieben ist. Sie können es jedoch auch problemlos an Java anpassen:
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) {
}
}
when:
testee.triggerSwitchExpression()
then:
AssertionError ae = thrown()
ae.message == "Unhandled switch case for enum value 'MY_ENUM_VALUE'"
cleanup:
switchMapField.set(null, originalSwitchMap)
In diesem Fall benötigen Sie überhaupt kein spöttisches Framework. Eigentlich würde es Ihnen sowieso nicht helfen, da kein mir bekanntes Mocking-Framework es Ihnen ermöglicht, einen Array-Zugriff zu verspotten. Sie könnten JMockit oder ein beliebiges Verspottungsframework verwenden, um den Rückgabewert von zu verspotten ordinal()
, aber dies würde wiederum einfach zu einem anderen Switch-Zweig oder einer AIOOBE führen.
Dieser Code, den ich gerade gezeigt habe, ist:
- Es durchläuft die anonymen Klassen innerhalb der Klasse, die den switch-Ausdruck enthält
- in denen sucht es nach dem Feld mit der Schalterzuordnung
- Wird das Feld nicht gefunden, wird die nächste Klasse ausprobiert
- Wenn a von
ClassNotFoundException
ausgelöst wird Class.forName
, schlägt der Test fehl, was beabsichtigt ist, da dies bedeutet, dass Sie den Code mit einem Compiler kompiliert haben, der einer anderen Strategie oder einem anderen Namensmuster folgt. Daher müssen Sie mehr Informationen hinzufügen, um verschiedene Compilerstrategien für das Einschalten abzudecken Aufzählungswerte. Wenn die Klasse mit dem Feld gefunden wird, break
verlässt sie die for-Schleife und der Test kann fortgesetzt werden. Diese ganze Strategie hängt natürlich davon ab, dass anonyme Klassen ab 1 und ohne Lücken nummeriert werden, aber ich hoffe, dass dies eine ziemlich sichere Annahme ist. Wenn Sie es mit einem Compiler zu tun haben, bei dem dies nicht der Fall ist, muss der Suchalgorithmus entsprechend angepasst werden.
- Wenn das Switch-Map-Feld gefunden wird, wird ein neues int-Array derselben Größe erstellt
- Das neue Array ist gefüllt,
Integer.MAX_VALUE
wodurch normalerweise der default
Fall ausgelöst werden sollte, solange Sie keine Aufzählung mit 2.147.483.647 Werten haben
- Das neue Array wird dem Switch-Map-Feld zugewiesen
- Die for-Schleife bleibt mit
break
- Jetzt kann der eigentliche Test durchgeführt werden, wodurch der auszuwertende Schalterausdruck ausgelöst wird
- Schließlich (in einem
finally
Block, wenn Sie Spock nicht verwenden, in einem cleanup
Block, wenn Sie Spock verwenden), um sicherzustellen, dass dies keine Auswirkungen auf andere Tests derselben Klasse hat, wird die ursprüngliche Switch-Map wieder in das Switch-Map-Feld eingefügt