Testen einer abstrakten Klasse mit JUnit

1. Übersicht

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, dass das Testen abstrakter Klassen fast immer die öffentliche API der konkreten Implementierungen durchlaufen sollte. 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 in Maven Central.

Powermock wird für Junit5 nicht vollständig unterstützt. Außerdem wird powermock-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 möchten die Methode defaultImpl () 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 die AbstractIndependent- Klasse 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 verwenden

Alternativ können wir Mockito 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 hierbei ist die Vorbereitung des Mocks zur Verwendung des realen Codes, wenn eine Methode mit Mockito.CALLS_REAL_METHODS aufgerufen wird .

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 dieselben zwei Ansätze wie zuvor verwenden - entweder eine konkrete Klasse erstellen oder Mockito verwenden, um ein Modell zu erstellen:

@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 wird abstractFunc () mit dem Rückgabewert versehen, den wir für den Test bevorzugen. Dies bedeutet, dass beim Aufrufen der nicht abstrakten Methode defaultImpl () 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 obstruierende Testmethode umgehen, bevor wir die Zielmethode testen:

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 defaultImpl () -Methode die private Methode getCurrentDateTime () auf . Diese private Methode erhält zur Laufzeit die aktuelle Zeit, die in unseren Unit-Tests vermieden werden sollte.

Um das Standardverhalten dieser privaten Methode zu verspotten, können wir Mockito nicht einmal verwenden, da es keine privaten Methoden steuern kann.

Stattdessen müssen wir PowerMock (verwenden n ote , dass dieses Beispiel funktioniert nur mit JUnit 4 , da die Unterstützung für diese Abhängigkeit nicht für JUnit 5 verfügbar ist ):

@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 den Läufer für den Test
  • @PrepareForTest (Klasse) weist PowerMock an, die Klasse für die spätere Verarbeitung vorzubereiten

Interessanterweise bitten wir PowerMock , die private Methode getCurrentDateTime () zu stoppen. PowerMock verwendet Reflection, um es zu finden, da es von außen nicht zugänglich ist.

Also , wenn wir nennen defaultImpl () , die Stub für eine private Methode erstellt wird anstelle der aktuellen Methode aufgerufen werden.

6. Nicht abstrakte Methode, die auf Instanzfelder zugreift

Abstrakte Klassen können einen internen Status haben, der mit Klassenfeldern implementiert ist. Der Wert der Felder kann einen erheblichen Einfluss auf die zu testende Methode haben.

Wenn ein Feld öffentlich oder geschützt ist, können wir über die Testmethode problemlos darauf zugreifen.

Aber wenn es privat ist, müssen wir PowerMockito 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"; } }

Here, the testFunc() method is using instance-level fields count and active before it returns.

When testing testFunc(), we can change the value of the count field by accessing instance created using Mockito.

On the other hand, to test the behavior with the private active field, we'll again have to use PowerMockito, and its Whitebox class:

@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()); }

We're creating a stub class using PowerMockito.mock(), and we're using Whitebox class to control object's internal state.

The value of the active field is changed to true.

7. Fazit

In diesem Tutorial haben wir mehrere Beispiele gesehen, die viele Anwendungsfälle abdecken. Abhängig vom Design können wir abstrakte Klassen in vielen weiteren Szenarien verwenden.

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 auf GitHub verfügbar.