Attrappen

Testen mit Mock-Objekten

Als Mockup bezeichnet man ein Vorführmodell oder auch eine Filmrequisite, die zwar aussieht wie ein Original, jedoch nicht über die ganze oder nur teilweise Funktionalitäten verfügt, die dieses bietet. Die verwendet man, wenn das Original entweder zu teuer (der zu bewerbende Prototyp), zu gefährlich (die zu entschärfende Bombe) oder schlicht unmöglich (das zu öffnende Stargate) ist.

In der Softwareentwicklung, speziell beim Softwaretest, ist ein Mockobjekt also ein Objekt, das sich verhält wie ein Original, jedoch nicht über die interne Logik desselben verfügt. Nötig ist dies vor allem bei Tests gegen Schnittstellen, bei denen man verhindern will, daß der Test von einer externen API abhängt oder diese nutzt, ohne daß dabei die Funktionalität der zu testenden Module eingeschränkt wird.

Das wären u.a. Internet-, Datenbank- oder Zugriffe auf das Dateisystem, die in einem echten Unittest ohnehin nichts zu suchen haben. Das Mock entpricht also einer Kulissenwand mit aufgemalten Türen und Fenstern oder einem Souffleur, der bereits alle zu gebenden Antworten kennt.

Zwei Arten von solchen Objekten finden dabei Anwendung:

  • Eine Implementierung des Originals, dessen Methoden immer dieselbe Antwort geben – entweder null oder einen konstanten Wert – nennt man Dummy, wie die beim Crashtest eingesetzten Puppen.
  • ‚Echte‘ Mocks sind Implementierungen, die auch eine gewisse Konfigurierbarkeit besitzen, d.h. deren Antworten zur Laufzeit eingestellt werden können.

Das Original kann sowohl eine normale oder abstrakte Klasse, als auch ein Interface sein, von dem dann das Mock abgeleitet wird.

Grundlagen: IoC & DI

Hauptvoraussetzung für eine saubere Strategie beim Testen ist, daß die Mock-Instanzen zur Laufzeit in die zu testenden Objekte eingebracht werden können. Aufrufe von new oder getInstance(), sowie der Aufruf von statischen Methoden innerhalb der Testobjekte sind demnach nicht erwünscht. Sollte einer dieser Fälle bereits im Code auftreten, ist definitiv über ein Refactoring nachzudenken.

  • Component comp = new Component();
  • Component comp = Component.getInstance();
  • public Component(Component comp) { … }
  • public setComponent(Component comp) { … };
  • @Inject @VisibleForTesting public Component comp;

Das Konzept der Befüllung über Parameter im Konstruktor oder einen Setter nennt sich Inversion of Control (Kontrollumkehrung), die Ausführung Dependecy Injection. Dies kann durch eigenen Code umgesetzt werden, oder über ein DI-Framework, wie z.B. Dagger. Letzteres erlaubt dann auch eine Testkonfiguration, bei der zum Start alle Schnittstellen automatisch durch Mocks ersetzt werden.

Kommen wir zur Mockobjekt-Klasse selber. Der einfachste Weg ist, einmal vom Original abzuleiten und alle public-Methoden zu überschreiben, bzw. implementieren. Bei Interfaces mit mehr als zehn Methoden kann da schon etwas mehr Arbeit entstehen, selbst wenn man nur ein oder zwei davon tatsächlich benötigt.

Aber zum Glück gibt es Frameworks, die einem genau diese Arbeit abnehmen. Der Vorteil ist zudem, daß dies per Reflection zur Laufzeit des Tests geschieht – es entsteht kein generierter Code, und die Mocks sind vollständig konfigurierbar. Als Bonus kann am Ende jeweils sogar eine Validierung durchlaufen werden, die prüft, ob auch alle konfigurierten Antworten wie erwartet abgerufen wurden.

EasyMock – http://easymock.org/

org.easymock:easymock:jar:4.0.2

Der Klassiker unter den Mock-Frameworks kommt mit einer JUnit-Rule, die das automatische Instanziieren der Mock-Objekte ermöglicht. Diese können aber auch per createMock(Component.class) selbst erzeugt werden.

import static org.easymock.Easymock.*;

public class EasyMockTest {
    @Rule public EasyMockRule rule = new EasyMockRule(this); // JUnit 4 Rule

    @Mock private Component mock;
    @TestSubject private ClassTested testedClass = new ClassTested();

    @Test public void testMethod() {
        expect(mock.method("Parameter")).andReturn(true); // prepare mock answer
        replay(mock);
        testedClass.testedMethod("Parameter", "value");   // internal call to mock.method()
        verify(mock);
    }
}

Der Test verläuft in vier Schritten: Nach der Vorbereitung des Mocks wird es in den replay()-Modus geschaltet, der Test durchlaufen und am Ende das verify() aufgerufen.

Mockito – https://site.mockito.org/

org.mockito:mockito-all:jar:2.0.2-beta

JUnit 5 liefert bereits im ersten Release eine passende Extension für Mockito mit, so daß auch hier die Erzeugung automatisch ablaufen kann.

import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class) // JUnit 5 Extension
public class MockitoTest {

    @Mock private Component mock;
    private ClassTested testedClass = new ClassTested();

    @Test public void testMethod() {
        when(mock.method("Parameter").thenReturn(true)  // prepare mock answer
        testedClass.testedMethod("Parameter", "value"); // internal call to mock.method()
        verify(mock);
    }
}

Zudem entfällt hier das Umschalten in den Replay-Modus, Mockito’s Objekte können praktisch über den gesamten Testverlauf neu konfiguriert und verifiziert werden.

Beide Mock-Frameworks bieten noch zusätzliche Möglichkeiten, wie u.a die Verwendung von Matchern im Aufruf einer Methode für unterschiedliche Antworten im gleichen Testlauf, und die Kontrolle der Prüfung der Anzahl und Reihenfolge der Aufrufe.

Doch was tun, wenn eine statische Klasse gemockt werden muß, die nicht im eigenen Projekt liegt? Manchmal ist man auf ältere Schnittstellen angewiesen und kann die Klassen nicht ohne weiteres anpassen. Hier hilft der Binford 2000 unter den Mock-Tools weiter:

PowerMock – https://powermock.github.io/

org.powermock:powermock-api-easymock:jar:2.0.2
org.powermock:powermock-api-mockito2:jar:2.0.2

PowerMock stellt seine Erweiterungen sowohl für EasyMock als auch Mockito bereit. Mit diesen ist es möglich, sowohl statische und private Methoden, als auch direkte Konstruktoren-Aufrufe mit new wegzumocken.

@PrepareForTest(Static.class)  // step 1
mockStatic(Static.class);      // step 2

EasyMock.expect(Static.staticMethod(param)).andReturn(value);
Mockito.when(Static.staticMethod(param)).thenReturn(value);

Dies geht allerdings mit einem Eingriff zur Laufzeit in den Bytecode der Klassen einher, was beim Testen (und auch sonst) nicht jedermanns Sache ist. Es solte also als letzter Ausweg angesehen werden, wenn alles andere scheitert. Zum Beispiel kann man statische Aufrufe auch in einem Delegate-Objekt kapseln, welches dann anstelle der Klasse gemockt wird.