Visit Linkwerk.com

Attrappen für finale Methoden mit PowerMock

Wer in der Software-Entwicklung konsequent Unittests durchführt, will auch die Interaktion der Komponenten mit ihrer Umgebung testen. Diese Umgebung vor dem Test aufzubauen ist nicht immer praktikabel oder gar nicht möglich.

In diesem Fall bieten sich sogenannte Mock-Objekte an. Diese Attrappen simulieren das Verhalten komplexer Objekte, indem sie deren Schnittstelle implementieren und bei Methodenaufrufen vorher festgelegte Werte zurückliefern. In der Java-Programmierung verwenden wir dafür EasyMock. EasyMock erstellt nicht nur simple Attrappen von Schnittstellen, sondern ermöglicht beispielsweise auch die Überprüfung, ob bestimmte Methoden an den Mock-Objekten aufgerufen werden. Beispiel:

@Test public void test() {
    // Mock-Objekt erzeugen
    SomeInterface mockObject = 
        EasyMock.createMock(SomeInterface.class);

    // erwartetes Verhalten aufzeichnen
    mockObject.doSomething(); 
    EasyMock.expectLastCall();

    // Umschalten in den Testmodus
    EasyMock.replay(mockObject); 
    ClassUnderTest objectToTest = new ClassUnderTest();
    objectToTest.methodUnderTest(mockObject); 
    
    // wirft Exception falls doSomething() nicht ausgeführt wurde
    EasyMock.verify(mockObject); 
}

Seit Version 3.0 enthält EasyMock auch die vorher nur als Erweiterung verfügbare Möglichkeit, Attrappen für einfache Klassen zu bauen. Allerdings lauern hier so manche Fallen. Z.B. ist es nicht möglich, das Verhalten von Methoden, die als final deklariert sind, zu simulieren. Das liegt daran, dass EasyMock versucht, von der Klasse abzuleiten und die Methoden zu überschreiben, was bei finalen Methoden nicht möglich ist.

Versucht man dies trotzdem, wird die eigentliche Methode der Klasse aufgerufen, was zu unerwartetem Verhalten (meistens einer wenig aussagekräftigen NullpointerException) führt. Falls die Methode ohne Exception durchlaufen wird, wirft EasyMock manchmal anschließend auch eine mehr oder weniger hilfreiche IllegalStateException. In den meisten Fällen steht man als Entwickler zunächst vor einem Rätsel, bis man sich doch noch einmal die Dokumentation (Abschnitt “Class Mocking Limitations”) zu Gemüte führt.

public class ClassToMock {
    // ...
    public final void doSomethingFinal() {
        //...
    }
}
public MyClassTest {
    @Test public void test() {
        ClassToMock mockObject = 
            EasyMock.createMock(ClassToMock.class);

        // entweder wird hier bereits eine Exception geworfen
        mockObject.doSomethingFinal();
        // oder EasyMock bemängelt hier "no last call on a mock available"
        EasyMock.expectLastCall();
        // ...
 }
}

Die einfache Lösung für dieses Problem ist, die betroffenen Methoden nicht zu finalisieren, auch wenn dies die meisten Code Quality Tools bemängeln.

Die saubere Lösung wäre es, die Interfaces von der Implementierung zu trennen und in den zu testenden Klassen nur mit den Interfaces zu arbeiten. Das Arbeiten mit Interfaces ist im allgemeinen sicherlich sinnvoll, führt jedoch auch dazu, dass man viel Zeit damit verbringt Boilerplate-Code zu schreiben und ob der Code dann wirklich in jedem Fall leichter wartbar ist, wage ich zu bezweifeln. Hier muss man Kosten und Nutzen im Einzelfall abwägen.

Sind die betroffenen Klassen Teil einer fremden Bibliothek, bliebe mir nur noch die Möglichkeit, diese Klassen in dem zu testenden Code nicht direkt zu benutzen, sondern indirekt über einen noch zu schreibenden Wrapper.

Möchte ich mir diesen Aufwand sparen, komme ich mit EasyMock alleine an dieser Stelle nicht weiter. Hier hilft PowerMock weiter. Das ist ein Framework, das EasyMock um einige Fähigkeiten erweitert, u.a. auch das simulieren von finalen und privaten Methoden. Dazu ergänzt man lediglich die Test-Klasse um folgende Annotationen. Ansonsten verwendet man PowerMock genauso wie EasyMock.

// weist JUnit an, den Test mit dem PowerMockRunner durchzuführen
@RunWith(PowerMockRunner.class)
// Hinweis für PowerMock die Klasse ClassToMock zu preparieren
@PrepareForTest( { ClassToMock.class })
public MyClassTestWithPowerMock {
    @Test public void test() {
        ClassToMock mockObject = 
            PowerMock.createMock(ClassToMock.class);
        mockObject.doSomethingFinal();
        PowerMock.expectLastCall();
        PowerMock.replay(mockObject);
        ClassUnderTest objectToTest = new ClassUnderTest();
        objectToTest.methodUnderTest(mockObject);

        PowerMock.replay(mockObject);
 }
}

Einen Haken hat aber auch diese Vorgehensweise: Das Preparieren der Klassen verzögert die Initialisierung des Tests deutlich um einige Sekunden (abhängig von der Komplexität der Klasse).


Comments are closed.