Testen einer abstrakten Klasse mit JUnit
1. Überblick
In diesem Tutorial analysieren wir verschiedene Anwendungsfälle und mögliche alternative Lösungen zum Unit-Test von abstrakten Klassen mit nicht abstrakten Methoden.
Beachten Sie, dasstesting abstract classes should almost always go through the public API of the concrete implementations ist. Wenden Sie daher die folgenden Techniken nur an, wenn Sie sicher sind, was Sie tun.
2. Maven-Abhängigkeiten
Beginnen wir mit Maven-Abhängigkeiten:
org.junit.jupiter
junit-jupiter-engine
5.1.0
test
org.mockito
mockito-core
2.8.9
test
org.powermock
powermock-module-junit4
1.7.4
test
junit
junit
org.powermock
powermock-api-mockito2
1.7.4
test
Die neuesten Versionen dieser Bibliotheken finden Sie unterMaven Central.
Powermock wird für Junit5 nicht vollständig unterstützt. Außerdem wirdpowermock-module-junit4 nur für ein in Abschnitt 5 dargestelltes Beispiel verwendet.
3. Unabhängige nicht abstrakte Methode
Betrachten wir einen Fall, in dem wir eine abstrakte Klasse mit einer öffentlichen nicht abstrakten Methode haben:
public abstract class AbstractIndependent {
public abstract int abstractFunc();
public String defaultImpl() {
return "DEFAULT-1";
}
}
Wir wollen die MethodedefaultImpl() testen und haben zwei mögliche Lösungen - mit einer konkreten Klasse oder mit Mockito.
3.1. Verwenden einer konkreten Klasse
Erstellen Sie eine konkrete Klasse, die dieAbstractIndependent -Skala erweitert, und testen Sie damit die Methode:
public class ConcreteImpl extends AbstractIndependent {
@Override
public int abstractFunc() {
return 4;
}
}
@Test
public void givenNonAbstractMethod_whenConcreteImpl_testCorrectBehaviour() {
ConcreteImpl conClass = new ConcreteImpl();
String actual = conClass.defaultImpl();
assertEquals("DEFAULT-1", actual);
}
Der Nachteil dieser Lösung ist die Notwendigkeit, die konkrete Klasse mit Dummy-Implementierungen aller abstrakten Methoden zu erstellen.
3.2. Mockito benutzen
Alternativ können wirMockito verwenden, um ein Modell zu erstellen:
@Test
public void givenNonAbstractMethod_whenMockitoMock_testCorrectBehaviour() {
AbstractIndependent absCls = Mockito.mock(
AbstractIndependent.class,
Mockito.CALLS_REAL_METHODS);
assertEquals("DEFAULT-1", absCls.defaultImpl());
}
Der wichtigste Teil hier ist daspreparation of the mock to use the real code when a method is invoked mitMockito.CALLS_REAL_METHODS.
4. Abstrakte Methode, die von der nicht abstrakten Methode aufgerufen wird
In diesem Fall definiert die nicht abstrakte Methode den globalen Ausführungsfluss, während die abstrakte Methode je nach Anwendungsfall auf verschiedene Arten geschrieben werden kann:
public abstract class AbstractMethodCalling {
public abstract String abstractFunc();
public String defaultImpl() {
String res = abstractFunc();
return (res == null) ? "Default" : (res + " Default");
}
}
Um diesen Code zu testen, können wir die gleichen zwei Ansätze wie zuvor verwenden - entweder eine konkrete Klasse erstellen oder Mockito zum Erstellen eines Mocks verwenden:
@Test
public void givenDefaultImpl_whenMockAbstractFunc_thenExpectedBehaviour() {
AbstractMethodCalling cls = Mockito.mock(AbstractMethodCalling.class);
Mockito.when(cls.abstractFunc())
.thenReturn("Abstract");
Mockito.doCallRealMethod()
.when(cls)
.defaultImpl();
assertEquals("Abstract Default", cls.defaultImpl());
}
Hier wirdabstractFunc() mit dem Rückgabewert versehen, den wir für den Test bevorzugen. Dies bedeutet, dass beim Aufrufen der nicht abstrakten MethodedefaultImpl() dieser Stub verwendet wird.
5. Nicht abstrakte Methode mit Testobstruktion
In einigen Szenarien ruft die zu testende Methode eine private Methode auf, die ein Testhindernis enthält.
Wir müssen die Testmethode "Behinderung" umgehen, bevor wir die Zielmethode testen können:
public abstract class AbstractPrivateMethods {
public abstract int abstractFunc();
public String defaultImpl() {
return getCurrentDateTime() + "DEFAULT-1";
}
private String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
}
In diesem Beispiel ruft die MethodedefaultImpl() die private MethodegetCurrentDateTime() auf. Diese private Methode liefert zur Laufzeit die aktuelle Zeit, die in unseren Unit-Tests vermieden werden sollte.
Um das Standardverhalten dieser privaten Methode zu verspotten, können wir nicht einmalMockito verwenden, da es keine privaten Methoden steuern kann.
Stattdessen müssen wirPowerMock (note that this example works only with JUnit 4 because support for this dependency isn’t available for JUnit 5) verwenden:
@RunWith(PowerMockRunner.class)
@PrepareForTest(AbstractPrivateMethods.class)
public class AbstractPrivateMethodsUnitTest {
@Test
public void whenMockPrivateMethod_thenVerifyBehaviour() {
AbstractPrivateMethods mockClass = PowerMockito.mock(AbstractPrivateMethods.class);
PowerMockito.doCallRealMethod()
.when(mockClass)
.defaultImpl();
String dateTime = LocalDateTime.now().toString();
PowerMockito.doReturn(dateTime).when(mockClass, "getCurrentDateTime");
String actual = mockClass.defaultImpl();
assertEquals(dateTime + "DEFAULT-1", actual);
}
}
Wichtige Bits in diesem Beispiel:
-
@RunWith definiert PowerMock als Läufer für den Test
-
@PrepareForTest(class) verkauft PowerMock, um die Klasse für die spätere Verarbeitung vorzubereiten
Interessanterweise bitten wirPowerMock, die private MethodegetCurrentDateTime(). zu stubben. SPowerMock verwendet Reflection, um sie zu finden, da sie von außen nicht zugänglich ist.
Wenn wir alsodefaultImpl() aufrufen, wird der für eine private Methode erstellte Stub anstelle der eigentlichen Methode aufgerufen.
6. Nicht abstrakte Methode, die auf Instanzfelder zugreift
Abstrakte Klassen können einen internen Status haben, der mit Klassenfeldern implementiert wird. Der Wert der Felder kann sich erheblich auf die zu testende Methode auswirken.
Wenn ein Feld öffentlich oder geschützt ist, können wir über die Testmethode leicht darauf zugreifen.
Wenn es jedoch privat ist, müssen wirPowerMockito verwenden:
public abstract class AbstractInstanceFields {
protected int count;
private boolean active = false;
public abstract int abstractFunc();
public String testFunc() {
if (count > 5) {
return "Overflow";
}
return active ? "Added" : "Blocked";
}
}
Hier verwendet die MethodetestFunc() die Feldercount undactive auf Instanzebene, bevor sie zurückgegeben wird.
Beim Testen vontestFunc() können wir den Wert des Feldscount ändern, indem wir auf eine mitMockito. erstellte Instanz zugreifen
Um das Verhalten mit dem privaten Feldactivezu testen, müssen wir erneutPowerMockito und dessen KlasseWhitebox verwenden:
@Test
public void whenPowerMockitoAndActiveFieldTrue_thenCorrectBehaviour() {
AbstractInstanceFields instClass = PowerMockito.mock(AbstractInstanceFields.class);
PowerMockito.doCallRealMethod()
.when(instClass)
.testFunc();
Whitebox.setInternalState(instClass, "active", true);
assertEquals("Added", instClass.testFunc());
}
Wir erstellen eine Stub-Klasse mitPowerMockito.mock() und verwenden die KlasseWhitebox, um den internen Status des Objekts zu steuern.
Der Wert des Feldsactive wird intrue geändert.
7. Fazit
In diesem Tutorial haben wir mehrere Beispiele gesehen, die viele Anwendungsfälle abdecken. Wir können abstrakte Klassen in vielen weiteren Szenarien verwenden, abhängig von dem Design, dem wir folgen.
Das Schreiben von Komponententests für abstrakte Klassenmethoden ist ebenso wichtig wie für normale Klassen und Methoden. Wir können jeden von ihnen mit verschiedenen Techniken oder verschiedenen verfügbaren Testunterstützungsbibliotheken testen.
Der vollständige Quellcode ist verfügbarover on GitHub.