Mockito – Vorbereitungen von Mocks – Teil 1

Einführung

Ein Einblick in die Verwendung von Mockito wurde bereits im ersten Teil „Mockito – 1 – Einführung“ gegeben.

In diesem Abschnitt – Mockito 2 – soll eine Beschreibung der gängigen Vorgehen gegeben werden, wie mit Mockito die Vorbereitung von Mocks durchgeführt werden kann.

Hierzu gehört:

  • Definieren von Verhalten eines Mocks (bzgl. Argumente und Rückgabewerte)
  • Werfen von Exceptions

Da allerdings das Thema des „Matchings“ der Argumente realtiv umfangreich ist, wird dieser Abschnitt des Beitrags auf zwei Teile aufgeteilt werden.

In diesem Teil soll es zunächst um die Konkretisierung der Problemstellung gehen.
Hierzu wird ein Beispiel genutzt, welches zunächst erklärt werden muss.
Allerdings basiert der Rest des Artikels darauf, wodurch dieser Teil zunächst notwendig ist.

Es wird auf ein paar Fallstricke aufmerksam gemacht, welche beim Testen unter gewissen häufigen Konstellationen aufkommen können. Daher wird dieser Teil zunächst etwas theoretischer werden.
Es folgen Beispiele für Umsetzungen, zur Vorbereitung von Mocks, um den Kriterien des jeweiligen Tests genüge zu tun.

Im zweiten Teil des Abschnitts wird das hier eingangs beschriebene „Szenario“ weiterverwendet werden, um dem Rest der Themen als Grundlage zu dienen.

Die UUT – Unit under Test

Um ein einheitliches Beispiel für die folgenden Darstellungen zu haben, folgt hier die Beschreibung der UUT, welche nur über den Einsatz aller im Nachfolgenden beschriebenen Techniken getestet werden kann / soll. Das Verhalten dieser Klasse ist teilweise dem Umstand geschuldet, dass hier verschiedene Problemstellungen aufkommen sollen. Sämtliche Sourcen sind frei „erfunden“. Ähnlichkeiten mit anderen Umsetzungen sind rein zufällig.

Leider ist der Code ein wenig umfangreicher, allerdings folgt die Erklärung dessen stante pede.
Empfehlenswert ist für den Vergleich ein Klick auf „Code in einem neuen Fenster anzeigen“ auf der eingeblendeten Kopfzeile des Code-Abschnitts.

Entgegen dem zu erwartenden TDD-Ansatz ( 😉 ) haben wir es für dieses Beispiel mit einer fertigen Umsetzung zu tun, welche getestet werden soll. Dies macht es hier einfacher die Abwägungen, welche hier beim Testen beachtet werden sollen und die Notwendigkeit für die eingesetzten Techniken nachzuvollziehen.

Die Methode „getTimeFromNtpCommunicatorForTimeZone“ befragt – abhängig von einer Konfiguration und der übergebenen Zeitzone – einen Service, welcher für NTP-Kommunikation (Network Time Protocol) verantwortlich ist. -> Sie wird somit – intern – eine Antwort erhalten.
Das Antwort-Objekt soll wiederum analysiert und in eine konkrete Datums- / Zeitangabe konvertiert werden.
Diese Datums- und Zeitangabe wird danach zurückgegeben. -> Sie kann also direkt im Test validiert werden.
Die Problematiken bei dieser Methode sind häufiger Natur:

  1. In der UUT werden intern – also unsichtbar von außen – Anfragen an Services gestellt, deren Rückgabewert nicht direkt validiert werden kann, sondern intern weiter genutzt wird
  2. Zwischenergebnisse anderer Mocks werden weitergenutzt und müssen daher valide sein
  3. Die Methode „createNtpRequestConfig“ der Klasse „NtpRequestConfigFactory“ ist statisch
    (siehe return-Statement der privaten Methode der UUT)
  4. Es gibt Methoden, welche Checked-Exceptions werfen
    (die Methode „getAnswerFromNtpServers“ wirft eine IOException)
  5. Genutzt werden sowohl primitive als auch komplexe Parameter-Typen

Die Methode „isInSyncWithNtpServer“ prüft hingegen die Übereinstimmung des Ergebnisses der oberen Methode (NTP-Datum) mit dem aktuellen Datum und der aktuellen Zeit (LocalDateTime.now()).
Die Problematiken bei dieser Methode wiederum sind einerseits die Wiederverwendung der Methode „getTimeFromNtpCommunicatorForTimeZone“ und deren Ergebnis, andererseits die Folgenden:

  • Es wird eine statische, native System-Komponente direkt genutzt.
    (Innerhalb der Implementierung von LocalDateTime.now() liegt der Aufruf von „currentTimeMillis()“, welche eine native function ist.)
  • Es wird eine Diskrepanz zwischen der aktuellen Uhrzeit und dem Resultat der „getTimeFromNtpCommunicator“–Methode geben.
    Die Vorbereitung des Tests und die tatsächliche Ausführung wird eine unbekannte und je nach Auslastung des Host-Systems bei Testausführung variierende Zeit verstreichen lassen.
    Dieses Delta muss berücksichtigt werden, um den erwarteten Parameter für den Aufruf der Methode this.timeComparationService.evaluateIsInSync zu bestimmen bzw. zu erwarten.

Beide Methoden eröffnen verschiedene Probleme beim Testen mit Mocks.
Auf die Probleme wird in den entsprechenden Abschnitten eingegangen.

Sowohl der ConfigrationService als auch der NtpCommunicator und NtpAnswerConversionService sind interfaces und hier komplett als Blackbox zu sehen.

Verhalten von Mock-Methoden definieren

Um Methoden in voller Gänze in ihrem Verhalten definieren zu können sind drei Aspekte wichtig:

  1. Welche Parameter erwartet die zu „mockende“ Methode?
  2. Wie konkret sind die zu erwartenden Parameter-Werte definierbar?
  3. Welcher Rückgabewert der gemockten Methode wird für die Testausführung benötigt, bzw. soll ein konkreter Methodenaufruf eine Exception hevorrufen?

Methoden-Parameter

Intern evaluiert das Mockito-Framework zur Laufzeit die tatsächlich beim Methodenaufruf übergebenen Argumente, um die zutreffende Antwort eines Mocks zu ermitteln.
Die jeweils registrierte Antwort (Answer) wird dann genutzt, um den Rückgabewerte dem Aufrufer (in erster Linie also die UUT) zu übermitteln.

Mit dieser Vorstellung im Hinterkopf ergeben sich die folgenden Problematiken:

  1. Es müssen primitive Typen und komplexe Typen gleichermaßen (auch gemischt) genutzt werden können
    (d.h Boxing und Unboxing darf kein Problem darstellen)
  2. Gleitkomma-Zahlen und Zeitangaben können erheblichen Deltas unterliegen und sollten eventuell trotzdem als „gleich“ angesehen werden
  3. Wenn ein Parameter-Wert zur Zeit der Test-Definition nicht bekannt ist (z.B. intern ermittelter Zufallswert), muss es über allgemeinere Definitionen möglich sein, eine Übereinstimmung der erwarteten (-> Test-SetUp) und tatsächlichen (-> Test-Durchführung) Parameter-Werte zu erreichen

Das Mockito-Framework bietet hierbei die nötige Funktionalität.

Im Folgenden werden die Unterschiedlichen Aufgabenbereiche bzw. die Lösungsstrategien für das Parameter-Matching dargestellt.

Ganze Werte und Objekte

Konkrete Parameter-Werte zu matchen ist im Wertebereich der ganzen Zahlen aus technischer Sicht kein Problem. Hier gibt es keine Ungenauigkeiten, Rundungsprobleme und keine weiteren Unwägbarkeiten (bis auf das Unboxing), vorausgesetzt, man hat die Quelle der Paramter-Werte selbst in der Hand. Ein einfaches Beispiel soll dies verdeutlichen:

Nun ist die Nützlichkeit oder Notwendigkeit der letzten Zeile offenbar umstritten.
Das Argument ist, dass sie keine offensichtliche Aussage trifft, da es
„ja schon verifiziert ist, dass die Methode aufgerufen wird, wenn der richtige Rückgabewert vorhanden ist“.
Daher sei das Mockito.verify() unnötig, um die Methode der UUT zu testen.
Allerdings wiegt – meiner Meinung nach – das folgende Argument genügend schwer:
Der tatsächliche Aufruf eines Mocks (verifiziert über das Mockito.verify()) ist ein wichtiges Indiz dafür,
dass die Herkunft des „configurationMock“-Parameters auch wirklich die „getTimeServiceConfiguration“-Methode ist.
Weiterhin darf nicht aus den Augen verloren werden, dass besonders Methoden, welche zwei/mehrere gleiche Parameter-Typen annehmen, nur so in ihrem korrekten Aufruf, mit korrekt sortierten Parametern verifiziert werden können.
Wie wir im weiteren Verlauf sehen, könnte der Aufruf auch auf das Benutzen von Matchern umgebaut werden, wodurch der Methodenaufruf nicht mehr sicher vor solchen Vertauschungs-Fehlern ist.

Dieselbe Art von „konkretem“ Matching wie bei integeren Werten ist das Matching von Objekten. Hier ist ebenfalls fest definiert, wie der erwartete Parameter aussieht.
Allerdings ist die interne Durchführung des Matchings ein anderes.

Objekte und Wrapper-Class-Instanzen werden über equals() verglichen.

Wie in diesem Beispiel zu sehen, wird der ntpCommunicationResultMock als Parameter erwartet. Nur dann, wenn der tatsächliche Aufruf-Parameter über equals() mit diesem ntpCommunicationResultMock true ergibt, wird der Result „dateTimeAnswer“ zurückgegeben. Andernfalls wäre es der Fallback-Wert (Default = hier NULL).

Matchen mit Matchers von Mockito

Die UUT eröffnet ein weiteres Problem:
Die statische Factory-Methode „NtpRequestConfigFactory.createNtpRequestConfig“ erstellt intern ein neues NtpRequestConfig-Objekt.

Somit ergibt sich die Frage:
Wie kann die neue Instanz aus der Factory-Methode für die Vorbereitung und Verifizierung der Methodenaufrufe im weiteren Test-Setup genutzt werden?

Antwort: In diesem Fall gar nicht :-).

Denn:

ergibt

Also würde das Folgende nicht funktionieren:

Es kann keine konkrete Parameter-Instanz für die Vorbereitung des Aufrufs von

herangezogen werden.

Es können allerdings Matcher eingesetzt werden, um das When und Verify durchzuführen.

Bzw.

Dieser Matcher repräsentiert die Gesamtheit aller Objekte, welche als NtpRequestConfig zuweisbar sind. Dieses Matching ist grob und undifferenziert, da es nur eine Typen-Übereinstimmung prüft.

Mockito ermöglicht es allerdings im Nachhinein die Argument-Instanzen auf Herz und Nieren zu überprüfen und behebt somit dieses Manko.
Auf die „ArgumentCaptor“s wird in einem andere Teil eingegangen werden.

Somit sieht der Testaufbau für diesen Teil wie folgt aus:

Für die Methode „Matchers.any“ gibt es weitere Ausführungen für die unterschiedlichsten Arten von Argumenten, insbesondere primitive Typen.

Hierzu zählen:

  1. anyBoolean()
  2. anyByte()
  3. anyInt()
  4. anyCollection()
  5. anyCollectionOf(Class<T> clazz)
  6. ect.

Hiermit soll zunächst Teil 1 des Abschnitts Vorbereitung von Mocks abgeschlossen sein.

Schluss und Ausblick

Die dargestellten Überlegungen und möglichen Lösungen sollen bei der Einarbeitung in Mockito als Mocking-Framework helfen.

Der Teil 2 des Abschnitts wird auf „additional Matchers“ eingehen, welche vom Mockito Framework bereitgestellt werden.
Weiterhin wird es um den Bereich der Rückgabewerte und Exceptions gehen.

 

Verfasst von Klaus-Werner Heinrich am 6. April 2017