Umgang mit statischen Dienstprogrammklassen beim Entwerfen für die Testbarkeit

62

Wir versuchen, unser System so zu gestalten, dass es testbar ist und größtenteils mit TDD entwickelt wird. Derzeit versuchen wir das folgende Problem zu lösen:

An verschiedenen Stellen müssen statische Hilfsmethoden wie ImageIO und URLEncoder (beide Standard-Java-API) sowie verschiedene andere Bibliotheken verwendet werden, die hauptsächlich aus statischen Methoden bestehen (wie die Apache Commons-Bibliotheken). Es ist jedoch äußerst schwierig, Methoden zu testen, die solche statischen Hilfsklassen verwenden.

Ich habe verschiedene Ideen, um dieses Problem zu lösen:

  1. Verwenden Sie ein Mock-Framework, das statische Klassen verspotten kann (wie PowerMock). Dies mag die einfachste Lösung sein, aber irgendwie fühlt es sich an, als würde man aufgeben.
  2. Erstellen Sie instanziierbare Wrapper-Klassen für alle diese statischen Dienstprogramme, damit sie in die Klassen eingefügt werden können, die sie verwenden. Das klingt nach einer relativ sauberen Lösung, aber ich befürchte, wir werden am Ende eine Menge solcher Wrapper-Klassen erstellen.
  3. Extrahieren Sie jeden Aufruf dieser statischen Hilfsklassen in eine Funktion, die überschrieben werden kann, und testen Sie eine Unterklasse der Klasse, die ich tatsächlich testen möchte.

Aber ich denke immer wieder, dass dies nur ein Problem sein muss, mit dem viele Menschen bei TDD konfrontiert sind - daher muss es bereits Lösungen für dieses Problem geben.

Was ist die beste Strategie, um Klassen, die diese statischen Helfer verwenden, testbar zu halten?

Benedikt
quelle
Ich bin mir nicht sicher, was Sie unter "glaubwürdigen und / oder offiziellen Quellen" verstehen, aber ich stimme dem zu, was @berecursive in seiner Antwort geschrieben hat. PowerMock gibt es aus einem bestimmten Grund und es sollte sich nicht nach "Aufgeben" anfühlen, insbesondere wenn Sie keine Wrapper-Klassen selbst schreiben möchten. Endgültige und statische Methoden sind ein Schmerz, wenn es um Unit-Tests (und TDD) geht. Persönlich? Ich verwende Methode 2, die Sie beschrieben haben.
Deco
"Glaubwürdige und / oder offizielle Quellen" ist nur eine der Optionen, die Sie beim Starten eines Kopfgeldes für eine Frage auswählen können. Was ich eigentlich meine: Erfahrungen aus oder Verweise auf Artikel von TDD-Experten. Oder jede Art von Erfahrung von jemandem, der das gleiche Problem hatte ...

Antworten:

34

(Keine "offiziellen" Quellen hier, ich fürchte - es gibt keine Spezifikation, wie man gut testet. Nur meine Meinungen, die hoffentlich nützlich sein werden.)

Wenn diese statischen Methoden echte Abhängigkeiten darstellen, erstellen Sie Wrapper. Also für Dinge wie:

  • ImageIO
  • HTTP-Clients (oder irgendetwas anderes im Zusammenhang mit dem Netzwerk)
  • Das Dateisystem
  • Abrufen der aktuellen Uhrzeit (mein Lieblingsbeispiel für die Abhängigkeitsinjektion)

... ist es sinnvoll eine Schnittstelle zu erstellen.

Aber viele der Methoden in Apache Commons sollten wahrscheinlich nicht verspottet / gefälscht werden. Nehmen Sie zum Beispiel eine Methode, um eine Liste von Zeichenfolgen zusammenzufügen und ein Komma dazwischen einzufügen. Es hat keinen Sinn , diese zu verspotten - lassen Sie einfach den statischen Anruf seine normale Arbeit erledigen. Sie möchten oder müssen das normale Verhalten nicht ersetzen. Sie haben es nicht mit einer externen Ressource oder etwas zu tun, mit dem Sie nur schwer arbeiten können, sondern nur mit Daten. Das Ergebnis ist vorhersehbar und Sie möchten nie, dass es etwas anderes ist als das, was es Ihnen sowieso gibt.

Ich vermute, dass Sie, nachdem Sie alle statischen Aufrufe entfernt haben, die wirklich praktische Methoden mit vorhersehbaren, "reinen" Ergebnissen (wie Base64 oder URL-Codierung) sind, anstatt Einstiegspunkte in ein ganzes Durcheinander logischer Abhängigkeiten (wie HTTP) zu finden, dass es vollkommen ist Es ist praktisch, mit den echten Abhängigkeiten das Richtige zu tun.

Jon Skeet
quelle
20

Dies ist definitiv eine mit einer Meinung versehene Frage / Antwort, aber für das, was es wert ist, dachte ich, ich würde meine zwei Cent einwerfen. In Bezug auf den TDD-Stil ist Methode 2 definitiv der Ansatz, der dem Brief folgt. Das Argument für Methode 2 ist, dass Sie, wenn Sie jemals die Implementierung einer dieser Klassen ersetzen wollten - sagen wir eine ImageIOäquivalente Bibliothek -, dies tun könnten, ohne das Vertrauen in die Klassen zu verlieren, die diesen Code nutzen.

Wie Sie bereits erwähnt haben, werden Sie, wenn Sie viele statische Methoden verwenden, am Ende eine Menge Wrapper-Code schreiben. Dies könnte auf lange Sicht keine schlechte Sache sein. In Bezug auf die Wartbarkeit gibt es sicherlich Argumente dafür. Persönlich würde ich diesen Ansatz bevorzugen.

Allerdings gibt es PowerMock aus einem bestimmten Grund. Es ist allgemein bekannt, dass das Testen mit statischen Methoden sehr schmerzhaft ist, weshalb PowerMock entwickelt wurde. Ich denke, Sie müssen Ihre Optionen dahingehend abwägen, wie viel Arbeit es sein wird, alle Ihre Helferklassen im Vergleich zur Verwendung von PowerMock zu beenden. Ich glaube nicht, dass es "aufgibt", PowerMock zu verwenden - ich habe nur das Gefühl, dass das Umschließen der Klassen Ihnen mehr Flexibilität in einem großen Projekt ermöglicht. Je mehr öffentliche Aufträge (Schnittstellen) Sie bereitstellen können, desto klarer wird die Trennung zwischen Absicht und Umsetzung.


quelle
1
Ein zusätzliches Problem, bei dem ich mir nicht sicher bin: Würden Sie beim Implementieren der Wrapper alle Methoden der Klasse implementieren, die umbrochen wird, oder nur die, die derzeit benötigt werden?
3
Wenn Sie agilen Ideen folgen, sollten Sie das Einfachste tun, was funktioniert, und es vermeiden, Arbeiten auszuführen, die Sie nicht benötigen. Aus diesem Grund sollten Sie nur die Methoden verfügbar machen, die Sie tatsächlich benötigen.
Assaf Stone
@AssafStone vereinbart
Seien Sie vorsichtig mit PowerMock, alle Klassenmanipulationen, die zum Verspotten der Methoden erforderlich sind, sind mit viel Aufwand verbunden. Ihre Tests werden viel langsamer, wenn Sie es ausgiebig verwenden.
bcarlso
Müssen Sie wirklich viel Wrapper schreiben, wenn Sie Ihre Tests / Migrationen mit der Einführung einer DI / IoC-Bibliothek koppeln möchten?
4

Als Referenz für alle, die sich ebenfalls mit diesem Problem befassen und auf diese Frage stoßen, werde ich beschreiben, wie wir beschlossen haben, das Problem anzugehen:

Wir folgen grundsätzlich dem als # 2 angegebenen Pfad (Wrapper-Klassen für statische Dienstprogramme). Wir verwenden sie jedoch nur, wenn es zu komplex ist, dem Dienstprogramm die erforderlichen Daten zur Erzielung der gewünschten Ausgabe bereitzustellen (dh wenn wir die Methode unbedingt verspotten müssen).

Dies bedeutet, dass wir für ein einfaches Dienstprogramm wie Apache Commons keinen Wrapper schreiben müssen StringEscapeUtils(da die benötigten Zeichenfolgen leicht bereitgestellt werden können) und wir keine Mocks für statische Methoden verwenden (wenn wir glauben, dass es Zeit zum Schreiben ist) eine Wrapper-Klasse und verspotten dann eine Instanz des Wrappers).

Benedikt
quelle
2

Ich würde diese Klassen mit Groovy testen . Groovy kann einfach zu jedem Java-Projekt hinzugefügt werden. Mit ihr können Sie die statischen Methoden ganz einfach verspotten. Ein Beispiel finden Sie unter Verspotten statischer Methoden mit Groovy .

Trevorismus
quelle
1

Ich arbeite für eine große Versicherungsgesellschaft und unser Quellcode enthält bis zu 400 MB reine Java-Dateien. Wir haben die gesamte Anwendung entwickelt, ohne an TDD zu denken. Ab Januar dieses Jahres haben wir mit dem Testen der einzelnen Komponenten begonnen.

Die beste Lösung in unserer Abteilung war die Verwendung von Mock-Objekten auf einigen systemabhängigen JNI-Methoden (geschrieben in C). Daher konnten Sie die Ergebnisse nicht jedes Mal auf jedem Betriebssystem genau abschätzen. Wir hatten keine andere Möglichkeit, als verspottete Klassen und spezielle Implementierungen von JNI-Methoden zu verwenden, um jedes einzelne Modul der Anwendung für jedes von uns unterstützte Betriebssystem zu testen.

Aber es war sehr schnell und es hat bisher ganz gut funktioniert. Ich empfehle es - http://www.easymock.org/


quelle
1

Die Objekte interagieren miteinander, um ein Ziel zu erreichen, wenn Sie ein Objekt haben, das aufgrund der Umgebung schwer zu testen ist (ein Webservice-Endpunkt, ein Dao-Layer, der auf die Datenbank zugreift, Controller, die http-Anforderungsparameter verarbeiten), oder Sie möchten Ihr Objekt dann isoliert testen Sie verspotten diese Objekte.

Die Notwendigkeit, statische Methoden zu verspotten, ist ein schlechter Geruch. Sie müssen Ihre Anwendung objektorientierter gestalten, und statische Methoden des Dienstprogramms zum Testen von Einheiten verleihen dem Projekt keinen Mehrwert. Die Wrapper-Klasse ist je nach Situation ein guter Ansatz, aber versuchen Sie es um die Objekte zu testen, die die statischen Methoden verwenden.


quelle
1

Manchmal verwende ich Option 4

  1. Verwenden Sie das Strategiemuster. Erstellen Sie eine Dienstprogrammklasse mit statischen Methoden, die die Implementierung an eine Instanz der steckbaren Schnittstelle delegieren. Codieren Sie einen statischen Initialisierer, der eine konkrete Implementierung einfügt. Schließen Sie zum Testen eine Scheinimplementierung an.

Etwas wie das.

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

Was mir an diesem Ansatz gefällt, ist, dass die Utility-Methoden statisch bleiben, was mir gerade recht ist, wenn ich versuche, die Klasse im gesamten Code zu verwenden.

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
bigh_29
quelle