Ich habe einen Enum- Schalter mehr oder weniger wie folgt:
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);
}
und ich möchte, dass alle Zeilen von den Tests abgedeckt werden, aber da erwartet wird, dass der Code alle Möglichkeiten abdeckt, kann ich keinen Wert ohne die entsprechende case-Anweisung im Schalter angeben.
Das Erweitern der Enumeration um einen zusätzlichen Wert ist nicht möglich, und das Verspotten der Methode equals für die Rückgabe false
funktioniert auch nicht, da der generierte Bytecode eine Sprungtabelle hinter den Vorhängen verwendet, um zum richtigen Fall zu gelangen ... Also habe ich mir gedacht dass vielleicht etwas schwarze Magie mit PowerMock oder so etwas erreicht werden könnte.
Vielen Dank!
bearbeiten :
Da mir die Aufzählung gehört, habe ich gedacht, ich könnte den Werten einfach eine Methode hinzufügen und so das Switch-Problem vollständig vermeiden. aber ich lasse die Frage, da es immer noch interessant ist.
quelle
throw
oderreturn
nach dem Wechsel nicht kompiliert .Antworten:
Wenn Sie Maven als Build-System verwenden können, können Sie einen viel einfacheren Ansatz verwenden. Definieren Sie einfach dieselbe Aufzählung mit einer zusätzlichen Konstante in Ihrem Testklassenpfad.
Angenommen, Sie haben Ihre Aufzählung wie folgt im Quellverzeichnis (src / main / java) deklariert:
package my.package; public enum MyEnum { A, B }
Jetzt deklarieren Sie genau dieselbe Aufzählung im Verzeichnis der Testquellen (src / test / java) wie folgt:
package my.package public enum MyEnum { A, B, C }
Die Tests sehen den Testklassenpfad mit der Aufzählung "überladen" und Sie können Ihren Code mit der Aufzählungskonstante "C" testen. Sie sollten dann Ihre IllegalArgumentException sehen.
Getestet unter Windows mit Maven 3.5.2, AdoptOpenJDK 11.0.3 und IntelliJ IDEA 2019.3.1
quelle
The type MyEnum is already defined
Hier ist ein vollständiges Beispiel.
Der Code ähnelt fast Ihrem Original (nur vereinfachte bessere Testvalidierung):
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); } }
Und hier ist der Komponententest mit vollständiger Codeabdeckung. Der Test funktioniert mit Powermock (1.4.10), Mockito (1.8.5) und 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 = mock(MyEnum.class); when(C.ordinal()).thenReturn(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)); } }
Ergebnis:
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.628 sec
quelle
Whitebox.setInternalState(C, "ordinal", 2);
es möglichfoo
statt zu bestehen2
. Und initialisierefoo
alsint foo = MyEnum.values().length;
erste Saite (obenMyEnum C = PowerMockito.mock(MyEnum.class);
)Anstatt eine radikale Bytecode-Manipulation zu verwenden, damit ein Test die letzte Zeile erreicht
foo
, würde ich ihn entfernen und mich stattdessen auf die statische Code-Analyse verlassen. Beispielsweise verfügt IntelliJ IDEA über die Codeinspektion "Enum-switch
Anweisung, bei der der Fall übersehen wird", die eine Warnung für diefoo
Methode erzeugt, wenn eine fehltcase
.quelle
throw
Anweisung redundant und kann entfernt werden, da das Fehlen einescase
inswitch
in von der IDE / dem Build erkannt wird.Wie Sie in Ihrer Bearbeitung angegeben haben, können Sie die Funktionalität in der Aufzählung selbst hinzufügen . Dies ist jedoch möglicherweise nicht die beste Option, da dies gegen das Prinzip "Eine Verantwortung" verstoßen kann. Eine andere Möglichkeit, dies zu erreichen, besteht darin, eine statische Zuordnung zu erstellen, die Aufzählungswerte als Schlüssel und die Funktionalität als Wert enthält. Auf diese Weise können Sie leicht testen, ob Sie für einen Aufzählungswert ein gültiges Verhalten haben, indem Sie alle Werte durchlaufen. Es könnte ein bisschen weit hergeholt an diesem Beispiel sein, aber das ist eine Technik , die ich oft zur Karte verwende Ressourcen - IDs zu Enum - Werten.
quelle
jMock (zumindest ab Version 2.5.1, die ich verwende) kann dies sofort tun. Sie müssen Ihre Mockery so einstellen, dass ClassImposterizer verwendet wird.
Mockery mockery = new Mockery(); mockery.setImposterizer(ClassImposterizer.INSTANCE); MyEnum unexpectedValue = mockery.mock(MyEnum.class);
quelle
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
undordinal
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 denvalues()
Aufruf wie zu verspottennew 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 // your test here cleanup: getPrivateFinalFieldForSetting.curry(MyEnum).with { it('$VALUES').set(null, originalEnumValues) }
Solange Sie sich nicht mit einem
switch
Ausdruck befassen , sondern mit einigenif
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 dendefault
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 WerteindizesMyEnum#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 WertNON_EXISTENT
im ersten Schritt oben erstellt werden würde, würden Sie entweder einen erhalten,ArrayIndexOutOfBoundsException
wenn Sie einenordinal
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ünschtendefault
Fall zu testen .Sie können dieses
int[]
Feld jetzt abrufen und so reparieren, dass es eine Zuordnung für das Orinal IhrerNON_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 einfachint[]
so bearbeiten , dass derdefault
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) { // 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)
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:
ClassNotFoundException
ausgelöst wirdClass.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.Integer.MAX_VALUE
wodurch normalerweise derdefault
Fall ausgelöst werden sollte, solange Sie keine Aufzählung mit 2.147.483.647 Werten habenbreak
finally
Block, wenn Sie Spock nicht verwenden, in einemcleanup
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ügtquelle
Zuallererst kann Mockito Scheindaten erstellen, die ganzzahlig sein können usw. Es kann keine richtige Aufzählung erstellen, da die Aufzählung eine bestimmte Anzahl von Ordnungsnamenwerten usw. hat. Wenn ich also eine Aufzählung habe
public enum HttpMethod { GET, POST, PUT, DELETE, HEAD, PATCH; }
Ich habe also insgesamt 5 Ordnungszahlen in enum HttpMethod, aber mockito weiß es nicht. Mockito erstellt die ganze Zeit Scheindaten und deren Null und Sie werden am Ende einen Nullwert übergeben. Hier wird also eine Lösung vorgeschlagen, bei der Sie die Ordnungszahl randomisieren und eine richtige Aufzählung erhalten, die für andere Tests bestanden werden kann
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()); } } } }
Ausgabe :
0 GET 0 GET 5 PATCH 5 PATCH 4 HEAD 5 PATCH 3 DELETE 0 GET 4 HEAD 2 PUT
quelle
Ich denke, dass der einfachste Weg, um die IllegalArgumentException zu erreichen, darin besteht, null an die foo-Methode zu übergeben, und Sie werden "Weiß nicht, wie man mit null umgeht" lesen.
quelle
switch
Aufgeben einesnull
Wertes ein ergibtNullPointerException
und nicht demdefault
Fall folgt (der im Beispiel bis nach dem durchfälltswitch
AussageIch habe meiner Aufzählung eine unbekannte Option hinzugefügt, die ich während des Tests übergebe. Nicht in jedem Fall ideal, aber einfach.
quelle
Ich würde den Standardfall mit einem von enum Fällen setzen:
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(); } }
quelle