Eine abstrakte Klasse mit JUnit testen

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.