Mit JMockit mühelos statische oder finale Methoden und Konstruktoren mocken

JMockit: Einfach. Alles. Mocken

Christian Helmbold

© Shutterstock.com / Elizabeth Spencer

Herkömmliche Mocking-Frameworks schränken die Möglichkeiten des Entwicklers ein: Statische oder finale Methoden und direkte Konstruktoraufrufe werden vermieden oder führen zu ungünstigen Tests, weil sie nicht ohne Weiteres gemockt werden können. Erweiterungen wie PowerMock lösen das Problem wenig elegant. Abhilfe schafft das mächtige Mocking-Framework JMockit.

Sollte ein Mocking-Framework bestimmen, ob statische Methoden verwendet werden dürfen, ob Klassen oder Methoden final sein dürfen, ob Schnittstellen (interface) eingeführt werden müssen, oder ob Objekte direkt mit new instanziiert werden können? Oder sollte der Entwickler besser selbst entscheiden, welches die beste Lösung ist?

In vielen Fällen enthalten externe Bibliotheken ohnehin schon Konstrukte, die sich mit altbekannten Mocking-Frameworks nicht mocken lassen. Zum Beispiel ist die Methode LocalDateTime#now() des JDK statisch und muss gemockt werden, wenn das Testergebnis nicht von der Uhrzeit der Testausführung abhängen soll.

Für Mockito und EasyMock gibt es die Erweiterung PowerMock, die in solchen Fällen aushelfen kann. Es ist allerdings wirklich eher eine Aushilfe als eine elegante Lösung:

  • Die Methoden unterscheiden sich von denen des Mocking-Frameworks und müssen zusätzlich gelernt werden.
  • Das Mocking-API insgesamt ist nicht mehr konsistent.
  • Die Reihenfolge der Methodenaufrufe ist anders als beim eigentlichen Mocking-Framework.
  • Zu mockende Klassen müssen mit @PrepareForTest(Some.class) über die Testklasse geschrieben werden. Falls es allerdings eine Klasse aus einer fremden Bibliothek (z. B. aus dem JDK) ist, muss an dieser Stelle stattdessen die Klasse stehen, die die zu mockende Klasse aufruft.
  • Die statische Typisierung bei Konstruktoren geht im Testcode verloren.

Dagegen können mit JMockit statische und finale Methoden oder Konstruktoren so einfach wie Instanzmethoden gemockt werden. Die Syntax ist genau die gleiche. Auch für void-Methoden, die Exceptions werfen, ist keine spezielle Syntax nötig. Das sind gleich mehrere Gründe, sich dieses ausgefeilte Mocking-Framework genauer anzusehen.

Auf die Plätze, fertig, los!

Um JMockit verwenden zu können, reicht es, das Framework in das Projekt einzufügen. Wer JUnit benutzt, muss darauf achten, JMockit vor JUnit in die Liste der Abhängigkeiten (z. B. in der pom.xml von Maven) zu setzen; bei TestNG spielt die Reihenfolge keine Rolle.

Mocks werden erzeugt, indem Felder oder Parameter mit der Annotation @Mocked ausgezeichnet werden. Ob ein Feld oder ein Methodenparameter verwendet wird, sollte davon abhängen, welches der kleinstmögliche Gültigkeitsbereich ist. Welche Aktionen auf den Mocks erwartet werden, wird in einem Expectations-Block beschrieben. Ein einfaches Beispiel, das die statische Methode LocalDateTime#now() mockt, zeigt Listing 1.

Listing 1
// zu testende Klasse
  public class NutritionalConsultant  {
  public static boolean isLunchTime() {
    int hour = LocalDateTime.now().getHour();
    return hour >= 12 && hour <= 14;
  }
}

public class NutritionalConsultantTest {

  @Test
  public void shouldReturnTrueFor12h(@Mocked LocalDateTime dateTime) {
    new Expectations() {{
      LocalDateTime.now(); result = dateTime;
      dateTime.getHour(); result = 12;
    }};

    boolean isLunchTime =  NutritionalConsultant.isLunchTime();

    assertThat(isLunchTime).isTrue();
  }
}

Deklarative Zauberei

Bei den doppelten geschweiften Klammern hinter new Expectations handelt es sich neben den normalen Klammern für den Rumpf der (anonymen) Klasse übrigens um einen ansonsten eher seltenen Instanzinitialisierer. Dieser Block ist im Prinzip ein namenloser Konstruktor und dient lediglich der Aufzeichnung der erwarteten Aufrufe in deklarativer Art und Weise; es werden an dieser Stelle keine Aktionen auf den tatsächlichen Klassen und Objekten ausgeführt. Hinter einem Aufruf wird der Rückgabewert in der Form result = value angegeben. Diese Angabe bezieht sich immer auf den vorigen Aufruf. Der Rückgabewert der Methode LocalDateTime#now() ist ein Mock der Klasse LocalDateTime. Der Rückgabewert der Methode getHour() ist der Wert 12. Mit der gleichen Syntax könnte an dieser Stelle übrigens auch eine Exception als Rückgabewert angegeben werden. Auch eine void-Methode, die eine Exception werfen würde, wäre syntaktisch kein Sonderfall. In JMockit ist die Syntax für all diese Fälle konsistent und einfach.

Technisch möglich ist diese aufgeräumte, aber auf den ersten Blick auch etwas rätselhafte Syntax durch Bytecode-Instrumentation. Nutzer von JMockit müssen sich darüber aber kaum Gedanken machen.

Testobjektpuzzle

Wenn die zu testende Klasse Referenzen auf Kollaborateure übergeben bekommt, kann JMockit die Erzeugung der Kollaborateur-Mocks und des Testobjekts übernehmen. Dazu wird @Injectable an die Felder oder Parameter, die Kollaborateure repräsentieren, und @Tested an das Testobjekt geschrieben. Listing 2 zeigt ein Beispiel, das Mocks der Klassen Service und Provider erzeugt, diese an den Konstruktor der ClassUnderTest übergibt und ein einsatzbereites Testobjekt dieser Klasse erzeugt.

Listing 2
public class SomeTest {
  @Injectable Service service;
  @Injectable Provider provider;
  @Tested ClassUnderTest testee;

  @Test
  public void shouldDoSomething() {
    ... 
    testee.perform();
    ... 
  }
}

Erwartungshaltung

JMockit unterstützt drei Stile zur Definition des Mock-Verhaltens und zur Prüfung der Interaktion mit den Mocks:

  1. (strict) Expectations: Alle Aufrufe auf den Mocks müssen vollständig und in exakter Reihenfolge stattfinden. Verifikation erfolgt implizit.
  2. NonStrictExpectations: Aufrufe auf den Mocks können in beliebiger Reihenfolge und Anzahl stattfinden. Normalerweise gibt es zusätzlich einen Verifications-Block. Dieser Stil entspricht dem von Mockito.
  3. Mockup: Verifikationen finden in den teilweise ausprogrammierten Mock-Objekten statt. Der Test wird aus der Perspektive des Kollaborateurs geschrieben. Dieser Stil wird in diesem Artikel nicht weiter erläutert, er ist jedoch ausführlich in der JMockit-Dokumentation beschrieben.

Strikte Erwartungen

Listing 3 zeigt den ersten Ansatz mit strikten Expectations. Im ExpectationsBlock sind alle Aufrufe auf den Mocks in der exakten Reihenfolge aufgeführt. Es darf keinen Aufruf mehr und keinen Aufruf weniger geben. Dadurch kann die korrekte Ausführung automatisch geprüft werden, ohne dass eine spezielle Methode zur Verifikation aufgerufen werden muss. Ein möglicher Nachteil dieses Stils ist allerdings, dass der Expectations-Block sehr viele Details enthält und den Test unter Umständen zu stark an die Implementierung binden kann.

Listing 3
@Mocked BookRepository

@Test
public void shouldAddNewBookToRepository(@Mocked Book book) {
  final String title = "Domain Driven Design";
  final String author = "Eric Evans";
  final int year = 2004;
  new Expectations() {{
    new Book(title, author, year); result = book;
    book.isValid(); result = true;
    BookRepository.add(book);
  }};
  Controller controller = new Controller(repository);
 
  controller.createBook(title, author, year);
 
  // keine explizite Verifikation nötig
}

Lockere Erwartungen

Die starke Verknüpfung von Test und Implementierung lässt sich durch NonStrictExpectations und Verifications oder durch beides vermeiden. Die Aufrufe auf den Mocks müssen dabei weder vollständig noch in genau der beschriebenen Reihenfolge stattfinden. Stattdessen wird das erwartete Verhalten entweder über die Prüfung des Ergebnisses oder mit einem zusätzlichen Verifications-Block erreicht. Sowohl in den NonStrictExpectations als auch in den Verifications können  Aufrufhäufigkeiten angegeben werden. Zum Beispiel könnte nach folgendem Muster festlegt werden, dass eine Methode 3- bis 5mal aufgerufen werden muss:

someMock.doIt(); minTimes = 3; maxTimes = 5;

In dem Beispiel in Listing 4 wird nur Price.random() gemockt und der definierte Preis als Rückgabewerte gesetzt (result = 12.34). Außerdem wird festgelegt, dass die Methode nur genau einmal aufgerufen werden darf (times = 1). Die Prüfung findet explizit in einem Verifications-Block statt. Dort wird geprüft, dass die Methode BookRepository.add(book) tatsächlich mit einem Buch aufgerufen wurde. Die Instanz der Klasse Book wird mit der Methode withCapture() der Klasse Verifications eingefangen und auf die Variable book gelegt. Anschließend können Eigenschaften dieses Objekts auf herkömmlichem Wege mit assertThat von AssertJ (oder ähnlichen APIs) geprüft werden.

Dieser Stil wird in den meisten Fällen am praktischsten sein, denn er balanciert strikte Prüfung und Freiheit der Implementierung aus. Dies ist auch der Stil, der bei Mockito verwendet wird.

Listing 4
public class BookControllerTest {

  @Mocked Price p;
  @Mocked BookRepository r;

  @Test
  public void shouldAddBookToRepository() {
    double price = 12.34;
    new NonStrictExpectations() {{
      Price.random(); result = price; times = 1;
    }};

    BookController.create("Eric Evans", "Domain Driven Design");

    new Verifications() {{
      Book book;
      BookRepository.add(book = withCapture());
      assertThat(book.price).isEqualTo(price);
    }};
  }
}

Verifications prüfen nicht die Aufrufreihenfolge. Wenn diese wichtig ist, können VerificationsInOrder anstelle von Verifications verwendet werden.

Argument Matching

Sowohl in Expectations– als auch in Verifications-Blöcken können Argumente flexibel gehandhabt werden. Das ist praktisch, wenn die Werte nicht genau bekannt sind oder wenn sie unwichtig für den Test sind. JMockit bietet zahlreiche Methoden für diesen Zweck, die mit any oder with beginnen. Die folgenden Methoden werden alle nach dem Muster someMock.doSomething(with…) in einen Expectations- oder VerificationsBlock geschrieben. Eine kleine Auswahl an Matchern:

  • withNotNull() prüft, ob ein Parameter nicht null ist
  • withAny(T arg) prüft, ob ein Parameter vom gleichen Typ wie das angegebene Objekt ist
  • withEqual(T arg) prüft anhand der equals-Methode, ob das Argument dem angegebenen Objekt entspricht
  • withMatch(CharSequence regex) prüft, ob eine Zeichenkette dem angegebenen Muster entspricht
  • withSameInstance(T arg) prüft, ob es sich bei dem Parameter um exakt die angegebene Instanz handelt
  • withCapture() fängt ein Objekt ein und bindet es an eine Variable; das so eingefangene Objekt kann dann genauer geprüft werden (Listing 4).

Besonders viel Flexibilität bietet die Methode with(Delegate), wobei Delegate ein Objekt ist, das die leere Schnittstelle Delegate<T> implementiert. Die Delegate-Klasse muss exakt eine nicht private Methode mit beliebigem Namen haben, die die gleichen (oder kompatible) Parameter wie die gemockte Methode haben sollte. Beispiel:

BookRepository.add(with(new Delegate<Book>() {  void check(Book book) {    assertThat(book.price).isNotNegative();  }}));

Etwas einfacher ist es bei any-Matchern: any steht für ein beliebiges Objekt, anyBoolean für einen beliebigen Wahrheitswert, anyInteger für eine beliebige Ganzzahl und so weiter. Ein erwarteter Methodenaufruf mit einem beliebigen Integer könnte so aussehen: someMock.doSomething(anyInt).

Fazit

Mocking von statischen und finalen Methoden ist mit JMockit so einfach und selbstverständlich, dass das Mocking-Framework die Entwickler nicht länger davon abhält, Methoden statisch oder final zu machen. Durch die so gewonnene Freiheit lässt sich Code in vielen Fällen radikal vereinfachen. Viele Schnittstellen existieren nur, weil die Implementierung durch einen Mock ersetzt werden soll. Solche Schnittstellen und der damit verbundene Overhead können durch ein mächtigeres Mocking-Framework entfallen.

Der deklarative Ansatz führt zu sehr klarem Mocking-Code und vermeidet Inkonsistenzen für Sonderfälle wie void-Methoden, die Exceptions werfen. JMockit kann alles, was Mockito kann – es kann aber noch mehr und ist eleganter. Es ist Zeit für Mocking der nächsten Generation!

Aufmacherbild: Northern Mockingbird via Shutterstock.com / Urheberrecht: Elizabeth Spencer

Geschrieben von
Christian Helmbold

Christian Helmbold arbeitet als Softwarearchitekt bei der Avantgarde Labs GmbH in Dresden. Sein Schwerpunkt liegt auf Softwarequalität.

Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: