Dieser Artikel vergleicht vier verschiedene Ansätze zum Testen privater Methoden in Java-Klassen.
Meine allererste Verwendung von JUnit bestand darin, ein Konformitätstestkit für die ServiceUI-API zu erstellen. Der Zweck eines Konformitätstestkits besteht darin, sicherzustellen, dass alternative Implementierungen derselben API mit der API-Spezifikation kompatibel sind. Da eine API-Spezifikation nur die öffentliche Schnittstelle der API und nicht die Implementierung der API definiert, führt ein Konformitätstest nur die öffentliche Schnittstelle aus. Mit anderen Worten, ein Konformitätstest ist ein „Black Box“ -Test. Es behandelt die zu testende API als Blackbox, deren externe Schnittstelle sichtbar ist, deren interne Implementierung jedoch nicht. Ein Konformitätstest einer Java-API muss daher nur auf die öffentlichen Member der zu testenden Pakete und Klassen zugreifen. Es besteht keine Notwendigkeit, auf Paketebene zuzugreifen, geschützt, oder private Mitglieder.
Als ich später JUnit auf die Aufgabe anwandte, tatsächliche Komponententests zu schreiben, im Gegensatz zu Konformitätstests, wollte ich White—Box-Tests schreiben – Tests, die Kenntnisse über die interne Implementierung der zu testenden Pakete und Klassen verwenden. Während ich in meinen Konformitätstests nur öffentliche Methoden testen wollte, wollte ich Komponententests für den Paketzugriff und gelegentlich private Methoden sowie öffentliche Methoden schreiben.
Daniel Steinberg zeigte mir die übliche JUnit-Technik der Verwendung paralleler Quellcodebäume, mit der ich Testklassen im selben Paket wie die zu testenden Klassen platzieren, sie jedoch in einem anderen Verzeichnis aufbewahren konnte. Dies ermöglichte eine saubere Trennung von Test- und Produktionscode. Durch Platzieren beider Quellbäume im KLASSENPFAD konnten meine Testklassen auf Methoden und Klassen auf Paketebene im zu testenden Paket zugreifen. Dies ließ mich jedoch immer noch mit dem Problem des Testens privater Methoden zurück.
Als ich Daniel nach dem Testen privater Methoden fragte, schlug er sanft vor, die privaten Methoden indirekt zu testen, indem ich die Paketzugriffs- und öffentlichen Methoden teste, die die privaten Methoden aufrufen. Diese Antwort hat mich nicht ganz befriedigt, weil ich gelegentlich wirklich den Drang verspürte, eine private Methode direkt zu testen. Meine ursprüngliche Lösung bestand darin, solche privaten Methoden nur für den Paketzugriff bereitzustellen, sodass ich sie direkt mit JUnit aus den Testklassen im selben Paket im parallelen Quellbaum testen konnte. Das funktionierte gut, aber ich fühlte mich irgendwie ein bisschen schmutzig. Obwohl ich im Allgemeinen entdeckte, dass das Nachdenken darüber, wie man Schnittstellen so gestaltet, dass sie leicht getestet werden können, mir half, bessere Schnittstellen zu entwerfen, hatte ich in diesem Fall das Gefühl, dass ich das Design etwas schlechter machte, um es testbar zu machen.
Als ich später an der Erstellung dessen teilnahm, was Frank Sommers, Matt Gerrans und ich schließlich als Artima SuiteRunner veröffentlichten , schwor ich, dass ich das Testen privater Methoden in SuiteRunner einfacher machen würde als in JUnit. Nachdem ich die verschiedenen Ansätze zum Testen privater Methoden untersucht hatte, entschied ich mich, in SuiteRunner nichts Besonderes zu tun, um das Testen privater Methoden zu unterstützen. Unabhängig davon, ob Sie JUnit oder SuiteRunner verwenden, haben Sie dieselben vier grundlegenden Ansätze zum Testen privater Methoden:
- Testen Sie keine privaten Methoden.
- Geben Sie dem Methodenpaket Zugriff.
- Verwenden Sie eine verschachtelte Testklasse.
- Verwenden Sie Reflexion.
In diesem Artikel werde ich diese vier Ansätze zum Testen privater Methoden in Java diskutieren. Ich werde mir die Vor- und Nachteile jedes Ansatzes ansehen und versuchen, etwas Licht darauf zu werfen, wann es sinnvoll ist, jeden Ansatz zu verwenden.
Wie ich in der Einleitung erwähnte, hörte ich zuerst den Rat, meinen gelegentlichen Drang, private Methoden zu testen, von Daniel Steinberg zu unterdrücken. Aber Daniel ist nicht nur die Quelle dieses Ratschlags, dem ich begegnet bin. Es scheint eine gemeinsame Haltung in der Java-Community zu sein. In den JUnit-FAQ heißt es beispielsweise:
Das Testen privater Methoden kann ein Hinweis darauf sein, dass diese Methoden in eine andere Klasse verschoben werden sollten, um die Wiederverwendbarkeit zu fördern.
Charles Miller drückte in seinem Weblog einen ähnlichen Standpunkt aus:
Wenn Sie eine gründliche Reihe von Tests für die exponierte (nicht private) Schnittstelle einer Klasse haben, sollten diese Tests ihrer Natur nach überprüfen, ob eine private Methode innerhalb der Klasse auch funktioniert. Wenn dies nicht der Fall ist oder wenn Sie eine private Methode haben, die so komplex ist, dass sie außerhalb des Kontexts ihrer öffentlichen Anrufer getestet werden muss, würde ich dies als Code-Geruch betrachten.
Und Dave Thomas und Andy Hunt schreiben in ihrem Buch Pragmatic Unit Testing:
Im Allgemeinen möchten Sie keine Kapselung zum Testen unterbrechen (oder wie Mama immer sagte: „Setzen Sie Ihre Privaten nicht aus!“). In den meisten Fällen sollten Sie in der Lage sein, eine Klasse zu testen, indem Sie ihre öffentlichen Methoden anwenden. Wenn sich hinter dem privaten oder geschützten Zugriff wichtige Funktionen verbergen, kann dies ein Warnzeichen dafür sein, dass eine andere Klasse Schwierigkeiten hat, herauszukommen.
Ich glaube all diesen Ratschlägen. In den meisten Fällen können private Methoden am effektivsten über Ansatz 1 getestet werden, indirekt durch Testen der geschützten und öffentlichen Methoden auf Paketebene, die sie aufrufen. Aber unweigerlich werden einige Leute in einigen Situationen das Gefühl haben, dass das direkte Testen einer privaten Methode das Richtige ist.
In meinem Fall neige ich dazu, viele private Utility-Methoden zu erstellen. Diese Dienstprogrammmethoden tun oft nichts mit Instanzdaten, sie arbeiten nur mit den übergebenen Parametern und geben ein Ergebnis zurück. Ich erstelle solche Methoden, um die aufrufende Methode verständlicher zu machen. Es ist eine Möglichkeit, die Komplexität der Implementierung der Klasse zu verwalten. Wenn ich nun die private Methode aus einer Methode extrahiere, die bereits funktioniert und eine gute Abdeckung durch Komponententests aufweist, reichen diese vorhandenen Komponententests wahrscheinlich aus. Ich muss nicht mehr Komponententests nur für die private Methode schreiben. Wenn ich jedoch die private Methode vor der aufrufenden Methode schreiben und die Komponententests vor dem Schreiben der privaten Methode schreiben möchte, möchte ich die private Methode wieder direkt testen. Im Fall von privaten Utility-Methoden habe ich nicht das Gefühl, dass mein Drang, die Methoden direkt zu testen, wie es in der JUnit-FAQ heißt, „ein Hinweis darauf ist, dass diese Methoden in eine andere Klasse verschoben werden sollten, um die Wiederverwendbarkeit zu fördern.“ Diese Methoden werden wirklich nur in der Klasse benötigt, in der sie sich befinden, und werden oft nur von einer anderen Methode aufgerufen.
Ein weiterer Grund, warum ich manchmal den Drang verspüre, private Methoden direkt zu testen, ist, dass ich mir Unit-Tests so vorstelle, als würde sie mir helfen, ein robustes System zu erreichen, indem ich dieses System aus robusten Teilen zusammenstelle. Jeder Teil ist eine „Einheit“, für die ich „Komponententests“ schreiben kann.“ Die Komponententests helfen mir sicherzustellen, dass jede Einheit korrekt funktioniert, was mir wiederum hilft, ein System aufzubauen, das als Ganzes korrekt funktioniert. Die primäre Einheit, an die ich beim Programmieren in Java denke, ist die Klasse. Ich baue Systeme aus Klassen, und Komponententests geben mir das Vertrauen, dass meine Klassen robust sind. Bis zu einem gewissen Grad empfinde ich aber auch die privaten Methoden, aus denen ich package-access-, protected- und public-Methoden zusammenstelle. Diese privaten Methoden sind Einheiten, die einzeln getestet werden können. Solche Komponententests geben mir die Gewissheit, dass die privaten Methoden korrekt funktionieren, was mir hilft, robuste, geschützte und öffentliche Methoden mit Paketzugriff zu erstellen.
Wie ich in der Einleitung erwähnt habe, war der Zugriff auf Methodenpakete mein erster Ansatz zum Testen privater Methoden mit JUnit. Dieser Ansatz funktioniert tatsächlich gut, ist aber mit geringen Kosten verbunden. Wenn ich einen privaten Zugriffsspezifizierer für eine Methode sehe, sagt er mir etwas, das ich gerne wissen möchte — dass dies Teil der Implementierung der Klasse ist. Ich weiß, dass ich die Methode ignorieren kann, wenn ich nur versuche, die Klasse aus einer anderen Klasse im Paket zu verwenden. Ich könnte dies über eine Paketzugriffsmethode herausfinden, indem ich mir den Namen, die Dokumentation und den Code der Methode genauer ansehe, aber das Wort private kommuniziert dies weitaus effizienter. Darüber hinaus ist das Hauptproblem, das ich mit diesem Ansatz habe, philosophisch. Obwohl es mir nichts ausmacht, „die Kapselung zum Testen zu brechen“, wie Dave und Andy es ausdrücken würden, fühle ich mich einfach nicht gut darin, die Kapselung so zu brechen, dass sich die API auf Paketebene ändert. Mit anderen Worten, obwohl ich ziemlich enthusiastisch bin, nicht öffentliche Methoden von Klassen zu testen, dh „White-Box“ -Komponententests zu erstellen, möchte ich lieber die API der zu testenden Klassen, einschließlich der API auf Paketebene, nicht geändert werden, um diese Tests zu erleichtern.
Ansatz 3: Verwenden einer verschachtelten Testklasse
Ein dritter Ansatz zum Testen privater Methoden besteht darin, eine statische Testklasse in der zu testenden Produktionsklasse zu verschachteln. Da eine verschachtelte Klasse Zugriff auf die privaten Member ihrer umschließenden Klasse hat, kann sie die privaten Methoden direkt aufrufen. Die statische Klasse selbst könnte Paketzugriff sein, sodass sie als Teil des White-Box-Tests geladen werden kann.
Der Nachteil dieses Ansatzes besteht darin, dass Sie, wenn Sie nicht möchten, dass auf die verschachtelte Testklasse in Ihrer Bereitstellungs-JAR-Datei zugegriffen werden kann, etwas zusätzliche Arbeit leisten müssen, um sie zu extrahieren. Einige Leute mögen es auch nicht, wenn Testcode in derselben Datei wie Produktionscode gemischt wird, obwohl andere diesen Ansatz bevorzugen.
Ansatz 4: Reflexion verwenden
Der vierte Ansatz zum Testen privater Methoden wurde mir von Vladimir R. Bossicard vorgeschlagen, der JUnit-Addons schrieb . Eines Tages während des Mittagessens erklärte Vladimir mir, dass die java.lang.reflect
-API Methoden enthielt, mit denen Client-Code den Zugriffsschutzmechanismus der Java Virtual Machine umgehen konnte. Er erzählte mir auch, dass sein JUnit-Addons-Projekt eine Klasse junitx.util.PrivateAccessor
enthielt, um die Reflection-API für genau diesen Zweck zu verwenden: Komponententests zu schreiben, die private Mitglieder der zu testenden Klassen manipulieren. Die JUnit FAQ verweist auf eine ähnliche Klasse namens PrivilegedAccessor
, geschrieben von Charlie Hubbard und Prashant Dhotke.
Ein Vorteil der Verwendung des Reflexionsansatzes zum Testen privater Methoden besteht darin, dass er eine saubere Trennung von Testcode und Produktionscode bietet. Die Tests müssen nicht wie in Ansatz 3 in der zu testenden Klasse verschachtelt sein. Vielmehr können sie neben den anderen Tests platziert werden, die die Paketebene und öffentliche Methoden der Klasse ausüben. Darüber hinaus müssen Sie die API der zu testenden Klasse nicht ändern. Im Gegensatz zu Ansatz 2 können private Methoden privat bleiben. Im Gegensatz zu Ansatz 3 müssen Sie auf Paketzugriffsebene keine zusätzliche verschachtelte Klasse hinzufügen. Der Hauptnachteil dieses Ansatzes besteht darin, dass der Testcode weitaus ausführlicher ist, da er die Reflection-API verwendet. Darüber hinaus sind Refactoring-IDEs wie Eclipse und IntelliJ normalerweise nicht so geschickt darin, die Namen von Methoden zu ändern, bei denen sie als String
bezeichnet werden, die an die Methoden der Reflection-API übergeben werden. Wenn Sie also den Namen der privaten Methode mit Ihrer Refactoring-IDE ändern, müssen Sie möglicherweise noch einige Änderungen im Testcode vornehmen.
Ein Beispiel
Um ein Beispiel für eine private Methode zu geben, die meiner Meinung nach direkte Komponententests verdient, habe ich einige Funktionen aus dermain
-Methode der Klasseorg.suiterunner.Runner
extrahiert.Runner.main
analysiert Befehlszeilenargumente und führt eine Reihe von Tests aus, wobei optional die GUI gestartet wird. Die Methode, die ich extrahiert habe,parseArgsIntoLists
, führt einen Teil der Analyse der Befehlszeilenargumente für die SuiteRunner-Anwendung durch. Um nun die öffentliche Methode zu testen, die diese private Methode aufruft, müsste ichmain
testen. Main ist natürlich die gesamte Anwendung, was das Testen der Methode ziemlich schwierig macht. Tatsächlich habe ich keinen vorhandenen Test fürmain
.
An diesem Punkt fragen Sie sich vielleicht, ob ich Tests zuerst im Stil der testgetriebenen Entwicklung geschrieben habe, wie habe ich jemals Parsing-Code geschrieben, der keine Unit-Tests hatte? Der Hauptgrund ist, dass meine Testinfektion schrittweise erfolgt ist. Ich habe tatsächlich eine Unit-Test-Grippe bekommen, lange bevor ich von JUnit oder Read Test Infected gehört hatte. Als ich beispielsweise Windows-Anwendungen in C ++ erstellte, schrieb ich ein wenig Code, um eine neu implementierte Methode zu testen, führte diesen Code dann aus und beobachtete seine Ausführung, indem ich die zu testende Methode mit dem Debugger durchlief. Diese Art von Komponententests hat mir geholfen, Robustheit zu erreichen, aber die Tests selbst haben nicht auf das richtige Verhalten überprüft. Ich habe selbst das richtige Verhalten überprüft, indem ich es über den Debugger beobachtet habe. Die Tests waren nicht automatisiert, und deshalb habe ich sie nicht gespeichert, damit sie später erneut ausgeführt werden können. Als ich Testergebnisse las, sah ich sofort den Wert, die Tests zu automatisieren und sie nach dem Refactoring als eine Art Regressionstest beizubehalten, aber es machte für mich lange Zeit keinen Sinn, die Tests zuerst zu schreiben. Ich wollte die Tests schreiben, nachdem ich die Funktionalität implementiert hatte, da ich die Tests mit dem Debugger ausgeführt hatte. Ein sekundärer Grund, warum ich bei der Entwicklung von SuiteRunner keine Tests geschrieben habe, ist, dass ich Suiterunners Tests mit SuiteRunner selbst schreiben wollte, um mein eigenes Hundefutter zu essen. Bis sich die grundlegende API von SuiteRunner erledigt hatte, hatte ich nicht das Test-Toolkit, mit dem ich die Tests schreiben wollte.
Seit dieser Zeit hat mich der Testvirus jedoch stärker erfasst, und ich ziehe es jetzt vor, die meiste Zeit zuerst Komponententests zu schreiben. Ich bevorzuge es, zuerst Tests zu schreiben, nicht so sehr, weil ich am Ende sauberere Designs habe, was normalerweise als Hauptvorteil der testgetriebenen Entwicklung beworben wird. Ich schreibe lieber zuerst Tests, weil ich oft feststelle, dass der Test nie geschrieben wird, wenn ich unter Druck in den Code eintauche, mit der Absicht, den Test später zu schreiben. SuiteRunner selbst hat zu diesem Zeitpunkt aus diesem Grund nur sehr wenige Unit-Tests. Hier ist die parseArgsIntoLists
-Methode:
private static void parseArgsIntoLists(String args, List runpathList, List reportersList, List suitesList) { if (args == null || runpathList == null || reportersList == null || suitesList == null) { throw new NullPointerException(); } for (int i = 0; i < args.length; i++) { if (args.startsWith("-p")) { runpathList.add(args); runpathList.add(args); ++i; } else if (args.startsWith("-g")) { reportersList.add(args); } else if (args.startsWith("-o")) { reportersList.add(args); } else if (args.startsWith("-e")) { reportersList.add(args); } else if (args.startsWith("-f")) { reportersList.add(args); reportersList.add(args); ++i; } else if (args.startsWith("-r")) { reportersList.add(args); reportersList.add(args); ++i; } else if (args.startsWith("-s")) { suitesList.add(args); do { ++i; suitesList.add(args); } while (i + 1 < args.length); } else { throw new IllegalArgumentException("Unrecognized argument: " + args); } } }
Die Befehlszeile für SuiteRunner enthält drei Arten von Informationen, die von SuiteRunner zum Ausführen von Tests verwendet werden: runpath, Reporter und suites. Die parseArgsIntoLists
-Methode durchläuft lediglich die Argumente, die als Array von String
übergeben werden, und platziert jedes Argument in einer der Listen runpathList
, reportersList
und suitesList
.
Bevor ich einen Test für diese private Methode schreibe, würde ich fragen, ob mein Drang, diesen Komponententest zu schreiben, einen Code-Geruch darstellt, wie Charles Miller es in seinem Weblog ausdrückte? Bedeutet dies, dass parseArgsIntoLists
in eine andere Klasse verschoben werden sollte, um die Wiederverwendbarkeit zu fördern, wie in der JUnit-FAQ vorgeschlagen? Würden Dave und Andy sagen, es ist ein Warnzeichen, dass es eine andere Klasse gibt, die darum kämpft, rauszukommen? Nun, vielleicht. Ich könnte konkret eine ArgumentsParser
-Klasse erstellen, die nur wenige statische Methoden enthält, die die Analysearbeit ausführen. Sowohl die ArgumentsParser
-Klasse als auch die darin enthaltenen Methoden könnten Paketzugriff sein, wodurch sie leicht zu testen wären. Aber das fühlt sich für mich einfach nicht richtig an. Diese Methoden werden nur von Runner.main
aufgerufen. Sie fühlen sich für mich eindeutig wie private Methoden an. Der einzige Grund, warum ich sie in eine ArgumentsParser
-Klasse verschieben würde, ist, sie testen zu können. Ich würde tatsächlich Ansatz Nummer 2 verwenden: Machen Sie das private Methodenpaket zugänglich.
Stattdessen habe ich mich für dieses Beispiel für Ansatz 4 entschieden und reflection . Ich schaute mir sowohl Vladimir Bossicards junitx.utils.PrivateAccessor
als auch Charlie Hubbards und Prashant Dhotkes PrivilegedAccessor
an, entschied aber, dass keiner von ihnen mir so half, wie ich es wollte. Zum einen können diese Klassen beide Felder testen, um sicherzustellen, dass sie korrekt festgelegt sind. Bisher habe ich noch nie den Drang verspürt, aus Komponententests direkt auf private Felder zuzugreifen. Ich möchte nur in der Lage sein, private Utility-Methoden zu testen. Das Hauptproblem, das ich mit diesen beiden Klassen hatte, ist jedoch, wie sie mit den Ausnahmen umgehen, die ausgelöst werden können, wenn versucht wird, die private Methode über Reflektion aufzurufen. Jede Klasse hat eine oder mehrere Methoden, deren Aufgabe es ist, eine Methode mit Reflexion aufzurufen. Die beiden invokeMethod
-Methoden von PrivilegedAccessor
übergeben jede Ausnahme an den Aufrufer zurück, einschließlich drei in der throws-Klausel deklarierter geprüfter Ausnahmen: NoSuchMethodException
, IllegalAccessException
und InvocationTargetException
. Im Gegensatz dazu fangen PrivateAccessor
’s zwei invoke
Methoden InvocationTargetException
ab und extrahieren und werfen die Zielausnahme, die tatsächliche Ausnahme, die von der aufgerufenen Methode ausgelöst wird. Es fängt dann jede andere Ausnahme ab und wirft NoSuchMethodException
. Mir hat es nicht gefallen, dass der Aufrufer von PrivilegedAccessor.invokeMethod
immer die drei geprüften Ausnahmen behandeln muss, da ich dachte, dass der allgemeine Weg, mit jeder Ausnahme umzugehen, darin besteht, den Test fehlschlagen zu lassen. Ich war auch besorgt, dass PrivateAccessor.invoke
potenziell nützliche Stack-Trace-Informationen in seiner Ausnahmebehandlungsrichtlinie wegwirft. Was ich wirklich wollte, war eine Methode, die versuchte, eine private Methode mit Reflektion aufzurufen, die jede ausgelöste Ausnahme neben InvocationTargetException
in eine ungeprüfte TestFailedException
. Meistens würde diese Ausnahme dazu führen, dass der Test fehlschlägt. In Tests, bei denen erwartet wurde, dass eine Ausnahme ausgelöst wird, konnte die in InvocationTargetException
enthaltene Ausnahme extrahiert und auf Richtigkeit getestet werden.
Deshalb schrieb ich invokeStaticMethod
. Mit dem Aufruf setAccessible(true)
kann die private Methode von außerhalb der Klasse aufgerufen werden. Eine entsprechende invokeStaticMethod
-Implementierung zur Verwendung mit JUnit würde AssertionFailedError
anstelle von TestFailedException
. Hier ist der Code:
private static void invokeStaticMethod(Class targetClass, String methodName, Class argClasses, Object argObjects) throws InvocationTargetException { try { Method method = targetClass.getDeclaredMethod(methodName, argClasses); method.setAccessible(true); method.invoke(null, argObjects); } catch (NoSuchMethodException e) { // Should happen only rarely, because most times the // specified method should exist. If it does happen, just let // the test fail so the programmer can fix the problem. throw new TestFailedException(e); } catch (SecurityException e) { // Should happen only rarely, because the setAccessible(true) // should be allowed in when running unit tests. If it does // happen, just let the test fail so the programmer can fix // the problem. throw new TestFailedException(e); } catch (IllegalAccessException e) { // Should never happen, because setting accessible flag to // true. If setting accessible fails, should throw a security // exception at that point and never get to the invoke. But // just in case, wrap it in a TestFailedException and let a // human figure it out. throw new TestFailedException(e); } catch (IllegalArgumentException e) { // Should happen only rarely, because usually the right // number and types of arguments will be passed. If it does // happen, just let the test fail so the programmer can fix // the problem. throw new TestFailedException(e); } }
Als nächstes habe ich eine Komfortmethode erstellt, die die bestimmte private Methode aufruft, die ich testen wollte:
private static void invokeParseArgsIntoLists(String args, List runpathList, List reportersList, List suitesList) throws InvocationTargetException { // Purposely pass null values to the method, to make sure it throws // NullPointerException Class argClasses = {String.class, List.class, List.class, List.class }; Object argObjects = {args, runpathList, reportersList, suitesList }; invokeStaticMethod(Runner.class, "parseArgsIntoLists", argClasses, argObjects); }
Endlich konnte ich Tests gegen die private Methode schreiben, ohne zu viel Unordnung zu verursachen, wie folgt:
public void testParseArgsIntoLists() throws InvocationTargetException { String args = new String; List runpathList = new ArrayList(); List reportersList = new ArrayList(); List suitesList = new ArrayList(); try { invokeParseArgsIntoLists(null, runpathList, reportersList, suitesList); fail(); } catch (InvocationTargetException e) { // throw the InvocationTargetException unless the target // exception is NullPointerException, which is expected Throwable targetException = e.getTargetException(); if (!(targetException instanceof NullPointerException)) { throw e; } } try { invokeParseArgsIntoLists(args, null, reportersList, suitesList); fail(); } catch (InvocationTargetException e) { // throw the InvocationTargetException unless the target // exception is NullPointerException, which is expected Throwable targetException = e.getTargetException(); if (!(targetException instanceof NullPointerException)) { throw e; } } try { invokeParseArgsIntoLists(args, runpathList, null, suitesList); fail(); } catch (InvocationTargetException e) { // throw the InvocationTargetException unless the target // exception is NullPointerException, which is expected Throwable targetException = e.getTargetException(); if (!(targetException instanceof NullPointerException)) { throw e; } } try { invokeParseArgsIntoLists(args, runpathList, reportersList, null); fail(); } catch (InvocationTargetException e) { // throw the InvocationTargetException unless the target // exception is NullPointerException, which is expected Throwable targetException = e.getTargetException(); if (!(targetException instanceof NullPointerException)) { throw e; } } args = new String; args = "-p"; args = "\"mydir\""; args = "-g"; args = "-f"; args = "test.out"; args = "-s"; args = "MySuite"; runpathList.clear(); reportersList.clear(); suitesList.clear(); invokeParseArgsIntoLists(args, runpathList, reportersList, suitesList); verify(runpathList.size() == 2); verify(runpathList.get(0).equals(args)); verify(runpathList.get(1).equals(args)); verify(reportersList.size() == 3); verify(reportersList.get(0).equals(args)); verify(reportersList.get(1).equals(args)); verify(reportersList.get(2).equals(args)); verify(suitesList.size() == 2); verify(suitesList.get(0).equals(args)); verify(suitesList.get(1).equals(args)); args = new String; args = "-p"; args = "\"mydir\""; args = "-e"; args = "-o"; args = "-r"; args = "MyCustomReporter"; args = "-s"; args = "MySuite"; args = "MyOtherSuite"; runpathList.clear(); reportersList.clear(); suitesList.clear(); invokeParseArgsIntoLists(args, runpathList, reportersList, suitesList); verify(runpathList.size() == 2); verify(runpathList.get(0).equals(args)); verify(runpathList.get(1).equals(args)); verify(reportersList.size() == 4); verify(reportersList.get(0).equals(args)); verify(reportersList.get(1).equals(args)); verify(reportersList.get(2).equals(args)); verify(reportersList.get(3).equals(args)); verify(suitesList.size() == 3); verify(suitesList.get(0).equals(args)); verify(suitesList.get(1).equals(args)); verify(suitesList.get(2).equals(args)); args = new String; args = "-p"; args = "\"serviceuitest-1.1beta4.jar myjini http://myhost:9998/myfile.jar\""; args = "-g"; args = "-s"; args = "MySuite"; args = "MySecondSuite"; args = "MyThirdSuite"; args = "MyFourthSuite"; args = "MyFifthSuite"; args = "MySixthSuite"; runpathList.clear(); reportersList.clear(); suitesList.clear(); invokeParseArgsIntoLists(args, runpathList, reportersList, suitesList); verify(runpathList.size() == 2); verify(runpathList.get(0).equals(args)); verify(runpathList.get(1).equals(args)); verify(reportersList.size() == 1); verify(reportersList.get(0).equals(args)); verify(suitesList.size() == 7); verify(suitesList.get(0).equals(args)); verify(suitesList.get(1).equals(args)); verify(suitesList.get(2).equals(args)); verify(suitesList.get(3).equals(args)); verify(suitesList.get(4).equals(args)); verify(suitesList.get(5).equals(args)); verify(suitesList.get(6).equals(args)); }
Fazit
Ansatz 1, private Methoden indirekt zu testen, indem die Paket-Level-, geschützten und öffentlichen Methoden getestet werden, die sie aufrufen, ist oft der beste Ansatz. In Fällen, in denen Sie private Methoden wirklich direkt testen möchten, bietet die Verwendung von Reflection zum Testen privater Methoden, obwohl dies ziemlich umständlich ist, die sauberste Trennung von Testcode und Produktionscode und die geringste Auswirkung auf den Produktionscode. Wenn es Ihnen jedoch nichts ausmacht, diese bestimmten privaten Methoden zu erstellen, mit denen Sie den Paketzugriff testen möchten, können Sie Ansatz 2 verwenden. Oder wenn es Ihnen nichts ausmacht, eine verschachtelte Testklasse in Ihrer zu testenden Produktionsklasse zu platzieren, können Sie mit Ansatz 3 zumindest die privaten Methoden privat halten.
Es gibt keine perfekte Antwort. Wenn Sie jedoch Ansatz 4 anwenden, erhalten Sie letztendlich eine Handvoll Methoden wie invokeStaticMethod
, die Sie wiederverwenden können. Sobald Sie eine bequeme Methode wie invokeParseArgsIntoLists
für eine private Methode geschrieben haben, können Sie ohne große Schwierigkeiten Tests für die private Methode schreiben.
Ressourcen
1. Die ServiceUI-API definiert eine Standardmethode zum Anhängen von Benutzeroberflächen an Jini-Dienste:
http://www.artima.com/jini/serviceui/index.html
2. Daniel Steinberg ist derzeit Chefredakteur von Java.NET:
http://www.java.net/
3. Artima SuiteRunner ist ein kostenloses Open-Source-Test-Toolkit und JUnit-Runner:
http://www.artima.com/suiterunner/index.html
4.JUnit FAQ Frage zum Testen privater Methoden:
http://junit.sourceforge.net/doc/faq/faq.htm#tests_10
5. Private Methoden testen (nicht tun), ein Weblog-Beitrag von Charles Miller:
http://fishbowl.pastiche.org/2003/03/28/testing_private_methods_dont_do_it
6. Andy Hunt und Dave Thomas sind die Autoren von Pragmatic Unit Testing, das im Pragmatic Store erhältlich ist.
7. JUnit Addons ist eine Sammlung von Hilfsklassen für JUnit erstellt von Vladimar R. Bossicard:
http://sourceforge.net/projects/junit-addons
PrivateAccessor
ist die Klasse aus JUnit-Addons, die das Testen privater Mitglieder erleichtert:
http://junit-addons.sourceforge.net/junitx/util/PrivateAccessor.html
9.PrivilegedAccessor Klasse, mit der Sie auf private Mitglieder zugreifen können:
http://groups.yahoo.com/group/junit/files/src/PrivilegedAccessor.java
10. Test Driven Development by Example, von Kent Beck, beschreibt die Test-First-Technik:
http://www.amazon.com/exec/obidos/ASIN/0321146530/
11. Testkit von Kent Beck und Erich Gamma führte JUnit in die Welt ein:
http://junit.sourceforge.net/doc/testinfected/testing.htm
Unit Testing Private Methods, ein Weblog-Beitrag über NUnit von Ted Graham:
http://weblogs.asp.net/tgraham/archive/2003/12/31/46984.aspx
Javas Zugriffsschutz für Unit-Tests untergraben, ein O’Reilly OnJava.com artikel von Ross Burton:
http://www.onjava.com/pub/a/onjava/2003/11/12/reflection.html
Die Klasse RunnerSuite
, aus der die Codeausschnitte in diesem Artikel stammen, wird hier vollständig angezeigt:
http://www.artima.com/suiterunner/privateExample.html
Warum wir JUnit überarbeitet haben
http://www.artima.com/suiterunner/why.html
Artima SuiteRunner Tutorial, Erstellen von Konformitäts- und Komponententests mit Artima SuiteRunner:
http://www.artima.com/suiterunner/tutorial.html
Erste Schritte mit Artima SuiteRunner, Wie man das einfache Beispiel ausführt, das in der Distribution enthalten ist:
http://www.artima.com/suiterunner/start.html
Ausführen von JUnit-Tests mit Artima SuiteRunner, Verwendung von Artima SuiteRunner als JUnit-Runner zum Ausführen Ihrer vorhandenen JUnit-Testsuiten:
http://www.artima.com/suiterunner/junit.html
Homepage der Familie Haas:
http://www.artima.com/suiterunner/index.html
Artima SuiteRunner Download-Seite (Sie müssen sich anmelden Artima.com um die Version herunterzuladen):
http://www.artima.com/suiterunner/download.jsp
Das SuiteRunner Forum:
http://www.artima.com/forums/forum.jsp?forum=61
Rede zurück!
Haben Sie eine Meinung? Leser haben bereits 26 Kommentare zu diesem Artikel gepostet. Warum nicht Ihre hinzufügen?
Über den Autor
Bill Venners ist Präsident von Artima Software, Inc. und Chefredakteur von Artima.com Er ist Autor des Buches Inside the Java Virtual Machine, einer programmierorientierten Übersicht über die Architektur und Interna der Java-Plattform. Seine beliebten Kolumnen im JavaWorld Magazine behandelten Java-Interna, objektorientiertes Design und Jini. Bill ist seit seiner Gründung in der Jini-Community aktiv. Er leitete das ServiceUI-Projekt der Jini-Community, das die ServiceUI-API produzierte. Die ServiceUI wurde zum De-facto-Standard für die Zuordnung von Benutzeroberflächen zu Jini-Diensten und war der erste Jini-Community-Standard, der über den Jini-Entscheidungsprozess genehmigt wurde. Bill ist auch gewähltes Mitglied des Initial Technical Oversight Committee (TOC) der Jini-Community und hat in dieser Rolle dazu beigetragen, den Governance-Prozess für die Community zu definieren. Derzeit widmet er den größten Teil seiner Energie dem Bauen Artima.com zu einer immer nützlicheren Ressource für Entwickler.