Best Practices von JUnit

JUnit ist ein typisches Toolkit: Bei sorgfältiger Verwendung und unter Berücksichtigung seiner Eigenheiten hilft JUnit bei der Entwicklung guter, robuster Tests. Blind verwendet, kann es einen Haufen Spaghetti anstelle einer Testsuite produzieren. Dieser Artikel enthält einige Richtlinien, die Ihnen helfen können, den Nudel-Albtraum zu vermeiden. Die Richtlinien widersprechen sich manchmal und einander - das ist absichtlich. Nach meiner Erfahrung gibt es selten feste Regeln in der Entwicklung, und Richtlinien, die behaupten, irreführend zu sein.

Wir werden auch zwei nützliche Ergänzungen zum Toolkit des Entwicklers genau untersuchen:

  • Ein Mechanismus zum automatischen Erstellen von Testsuiten aus Klassendateien in einem Teil eines Dateisystems
  • Eine neue TestCase, die Tests in mehreren Threads besser unterstützt

Bei Unit-Tests erstellen viele Teams eine Art Test-Framework. JUnit, das als Open Source verfügbar ist, beseitigt diese lästige Aufgabe, indem es ein vorgefertigtes Framework für Unit-Tests bereitstellt. JUnit, das am besten als integraler Bestandteil eines Entwicklungstestregimes verwendet wird, bietet einen Mechanismus, mit dem Entwickler Tests konsistent schreiben und ausführen können. Was sind die Best Practices von JUnit?

Verwenden Sie nicht den Testfallkonstruktor, um einen Testfall einzurichten

Das Einrichten eines Testfalls im Konstruktor ist keine gute Idee. Erwägen:

öffentliche Klasse SomeTest erweitert TestCase public SomeTest (String testName) {super (testName); // Testeinrichtung durchführen}}

Stellen Sie sich vor, der Setup-Code löst während des Setups ein IllegalStateException. Als Antwort würde JUnit eine auslösen, AssertionFailedErrorwas darauf hinweist, dass der Testfall nicht instanziiert werden konnte. Hier ist ein Beispiel für die resultierende Stapelverfolgung:

junit.framework.AssertionFailedError: Testfall kann nicht instanziiert werden: test1 bei junit.framework.Assert.fail (Assert.java:143) bei junit.framework.TestSuite.runTest (TestSuite.java:178) bei junit.framework.TestCase.runBare (TestCase.java:129) unter junit.framework.TestResult.protect (TestResult.java:100) unter junit.framework.TestResult.runProtected (TestResult.java:117) unter junit.framework.TestResult.run (TestResult.java: 103) unter junit.framework.TestCase.run (TestCase.java:120) unter junit.framework.TestSuite.run (TestSuite.java, kompilierter Code) unter junit.ui.TestRunner2.run (TestRunner.java:429) 

Diese Stapelverfolgung erweist sich als ziemlich uninformativ; Es zeigt nur an, dass der Testfall nicht instanziiert werden konnte. Der Ort oder der Ursprungsort des ursprünglichen Fehlers werden nicht detailliert angegeben. Dieser Mangel an Informationen macht es schwierig, die zugrunde liegende Ursache der Ausnahme abzuleiten.

Anstatt die Daten im Konstruktor einzurichten, führen Sie die Testeinrichtung durch Überschreiben durch setUp(). Jede darin ausgelöste Ausnahme setUp()wird korrekt gemeldet. Vergleichen Sie diesen Stack-Trace mit dem vorherigen Beispiel:

java.lang.IllegalStateException: Ups bei bp.DTC.setUp (DTC.java:34) bei junit.framework.TestCase.runBare (TestCase.java:127) bei junit.framework.TestResult.protect (TestResult.java:100) at junit.framework.TestResult.runProtected (TestResult.java:117) at junit.framework.TestResult.run (TestResult.java:103) ... 

Diese Stapelverfolgung ist viel informativer. Es zeigt, welche Ausnahme ausgelöst wurde ( IllegalStateException) und von wo. Das macht es viel einfacher, den Fehler des Testaufbaus zu erklären.

Nehmen Sie nicht die Reihenfolge an, in der Tests innerhalb eines Testfalls ausgeführt werden

Sie sollten nicht davon ausgehen, dass Tests in einer bestimmten Reihenfolge aufgerufen werden. Betrachten Sie das folgende Codesegment:

öffentliche Klasse SomeTestCase erweitert TestCase {public SomeTestCase (String testName) {super (testName); } public void testDoThisFirst () {...} public void testDoThisSecond () {}}

In diesem Beispiel ist nicht sicher, ob JUnit diese Tests in einer bestimmten Reihenfolge ausführt, wenn Reflection verwendet wird. Das Ausführen der Tests auf verschiedenen Plattformen und Java-VMs kann daher zu unterschiedlichen Ergebnissen führen, es sei denn, Ihre Tests sind so konzipiert, dass sie in beliebiger Reihenfolge ausgeführt werden. Durch das Vermeiden einer zeitlichen Kopplung wird der Testfall robuster, da Änderungen in der Reihenfolge keine Auswirkungen auf andere Tests haben. Wenn die Tests gekoppelt sind, sind die Fehler, die sich aus einer geringfügigen Aktualisierung ergeben, möglicherweise schwer zu finden.

Verwenden Sie in Situationen, in denen das Bestellen von Tests sinnvoll ist - wenn es für Tests effizienter ist, mit gemeinsam genutzten Daten zu arbeiten, die bei jedem Test einen neuen Status herstellen - eine statische suite()Methode wie diese, um die Reihenfolge sicherzustellen:

public static Test suite () {suite.addTest (neues SomeTestCase ("testDoThisFirst";)); suite.addTest (neuer SomeTestCase ("testDoThisSecond";)); Rückkehr Suite; }}

In der JUnit-API-Dokumentation gibt es keine Garantie für die Reihenfolge, in der Ihre Tests aufgerufen werden, da JUnit Vectorzum Speichern von Tests ein verwendet. Sie können jedoch davon ausgehen, dass die oben genannten Tests in der Reihenfolge ausgeführt werden, in der sie der Testsuite hinzugefügt wurden.

Schreiben Sie keine Testfälle mit Nebenwirkungen

Testfälle mit Nebenwirkungen weisen zwei Probleme auf:

  • Sie können Daten beeinflussen, auf die sich andere Testfälle stützen
  • Sie können Tests nicht ohne manuellen Eingriff wiederholen

In der ersten Situation kann der einzelne Testfall korrekt funktionieren. Wenn es jedoch in einen TestSuiteTestfall integriert ist , der jeden Testfall auf dem System ausführt, können andere Testfälle fehlschlagen. Dieser Fehlermodus kann schwierig zu diagnostizieren sein, und der Fehler kann weit vom Testfehler entfernt sein.

In der zweiten Situation hat ein Testfall möglicherweise einen Systemstatus aktualisiert, sodass er ohne manuellen Eingriff nicht erneut ausgeführt werden kann. Dies kann beispielsweise darin bestehen, Testdaten aus der Datenbank zu löschen. Überlegen Sie sorgfältig, bevor Sie manuell eingreifen. Zunächst muss der manuelle Eingriff dokumentiert werden. Zweitens konnten die Tests nicht mehr unbeaufsichtigt ausgeführt werden, sodass Sie nicht mehr über Nacht oder im Rahmen eines automatisierten regelmäßigen Testlaufs Tests ausführen können.

Rufen Sie bei der Unterklasse die Methoden setUp () und tearDown () einer Oberklasse auf

Wenn Sie überlegen:

öffentliche Klasse SomeTestCase erweitert AnotherTestCase {// Eine Verbindung zu einer privaten Datenbank Database theDatabase; public SomeTestCase (String testName) {super (testName); } public void testFeatureX () {...} public void setUp () {// Datenbank löschen theDatabase.clear (); }}

Können Sie den absichtlichen Fehler erkennen? setUp()sollte aufrufen, super.setUp()um sicherzustellen, dass die in AnotherTestCaseinitialisierte Umgebung initialisiert wird. Natürlich gibt es Ausnahmen: Wenn Sie die Basisklasse so gestalten, dass sie mit beliebigen Testdaten arbeitet, gibt es kein Problem.

Laden Sie keine Daten von fest codierten Speicherorten in ein Dateisystem

Tests müssen häufig Daten von einem Speicherort im Dateisystem laden. Folgendes berücksichtigen:

public void setUp () {FileInputStream inp ("C: \\ TestData \\ dataSet1.dat"); ...}

Der obige Code basiert auf dem Datensatz im C:\TestDataPfad. Diese Annahme ist in zwei Situationen falsch:

  • Ein Tester hat keinen Platz zum Speichern der Testdaten C:und zum Speichern auf einer anderen Festplatte
  • Die Tests werden auf einer anderen Plattform wie Unix ausgeführt

Eine Lösung könnte sein:

public void setUp () {FileInputStream inp ("dataSet1.dat"); ...}

Diese Lösung hängt jedoch davon ab, dass der Test aus demselben Verzeichnis wie die Testdaten ausgeführt wird. Wenn dies von mehreren verschiedenen Testfällen angenommen wird, ist es schwierig, sie in eine Testsuite zu integrieren, ohne das aktuelle Verzeichnis ständig zu ändern.

Um das Problem zu lösen, greifen Sie mit Class.getResource()oder auf den Datensatz zu Class.getResourceAsStream(). Ihre Verwendung bedeutet jedoch, dass Ressourcen von einem Ort relativ zum Ursprung der Klasse geladen werden.

Testdaten sollten nach Möglichkeit mit dem Quellcode in einem Konfigurationsmanagementsystem (CM) gespeichert werden. Wenn Sie jedoch den oben genannten Ressourcenmechanismus verwenden, müssen Sie ein Skript schreiben, das alle Testdaten vom CM-System in den Klassenpfad des zu testenden Systems verschiebt. Ein weniger umständlicher Ansatz besteht darin, die Testdaten zusammen mit den Quelldateien im Quellbaum zu speichern. Bei diesem Ansatz benötigen Sie einen standortunabhängigen Mechanismus, um die Testdaten im Quellbaum zu lokalisieren. Ein solcher Mechanismus ist eine Klasse. Wenn eine Klasse einem bestimmten Quellverzeichnis zugeordnet werden kann, können Sie folgenden Code schreiben:

InputStream inp = SourceResourceLoader.getResourceAsStream (this.getClass (), "dataSet1.dat"); 

Jetzt müssen Sie nur noch festlegen, wie eine Klasse dem Verzeichnis zugeordnet werden soll, das die entsprechende Quelldatei enthält. Sie können den Stamm des Quellbaums (vorausgesetzt, er hat einen einzelnen Stamm) anhand einer Systemeigenschaft identifizieren. Der Paketname der Klasse kann dann das Verzeichnis identifizieren, in dem sich die Quelldatei befindet. Die Ressource wird aus diesem Verzeichnis geladen. Für Unix und NT ist die Zuordnung unkompliziert: Ersetzen Sie jede Instanz von '.' mit File.separatorChar.

Bewahren Sie Tests am selben Ort wie den Quellcode auf

Wenn sich die Testquelle am selben Speicherort wie die getesteten Klassen befindet, werden sowohl Test als auch Klasse während eines Builds kompiliert. Dies zwingt Sie, die Tests und Klassen während der Entwicklung synchron zu halten. In der Tat werden Unit-Tests, die nicht Teil des normalen Builds sind, schnell veraltet und unbrauchbar.

Namenstests richtig

Name the test case TestClassUnderTest. For example, the test case for the class MessageLog should be TestMessageLog. That makes it simple to work out what class a test case tests. Test methods' names within the test case should describe what they test:

  • testLoggingEmptyMessage()
  • testLoggingNullMessage()
  • testLoggingWarningMessage()
  • testLoggingErrorMessage()

Proper naming helps code readers understand each test's purpose.

Ensure that tests are time-independent

Where possible, avoid using data that may expire; such data should be either manually or programmatically refreshed. It is often simpler to instrument the class under test, with a mechanism for changing its notion of today. The test can then operate in a time-independent manner without having to refresh the data.

Consider locale when writing tests

Consider a test that uses dates. One approach to creating dates would be:

Date date = DateFormat.getInstance ().parse ("dd/mm/yyyy"); 

Unfortunately, that code doesn't work on a machine with a different locale. Therefore, it would be far better to write:

Calendar cal = Calendar.getInstance (); Cal.set (yyyy, mm-1, dd); Date date = Calendar.getTime (); 

The second approach is far more resilient to locale changes.

Utilize JUnit's assert/fail methods and exception handling for clean test code

Many JUnit novices make the mistake of generating elaborate try and catch blocks to catch unexpected exceptions and flag a test failure. Here is a trivial example of this:

public void exampleTest () { try { // do some test } catch (SomeApplicationException e) { fail ("Caught SomeApplicationException exception"); } } 

JUnit automatically catches exceptions. It considers uncaught exceptions to be errors, which means the above example has redundant code in it.

Here's a far simpler way to achieve the same result:

public void exampleTest () throws SomeApplicationException { // do some test } 

In this example, the redundant code has been removed, making the test easier to read and maintain (since there is less code).

Use the wide variety of assert methods to express your intention in a simpler fashion. Instead of writing:

assert (creds == 3); 

Write:

assertEquals ("The number of credentials should be 3", 3, creds); 

The above example is much more useful to a code reader. And if the assertion fails, it provides the tester with more information. JUnit also supports floating point comparisons:

assertEquals ("some message", result, expected, delta); 

When you compare floating point numbers, this useful function saves you from repeatedly writing code to compute the difference between the result and the expected value.

Use assertSame() to test for two references that point to the same object. Use assertEquals() to test for two objects that are equal.

Document tests in javadoc

Test plans documented in a word processor tend to be error-prone and tedious to create. Also, word-processor-based documentation must be kept synchronized with the unit tests, adding another layer of complexity to the process. If possible, a better solution would be to include the test plans in the tests' javadoc, ensuring that all test plan data reside in one place.

Avoid visual inspection

Testing servlets, user interfaces, and other systems that produce complex output is often left to visual inspection. Visual inspection -- a human inspecting output data for errors -- requires patience, the ability to process large quantities of information, and great attention to detail: attributes not often found in the average human being. Below are some basic techniques that will help reduce the visual inspection component of your test cycle.

Swing

When testing a Swing-based UI, you can write tests to ensure that:

  • All the components reside in the correct panels
  • You've configured the layout managers correctly
  • Text widgets have the correct fonts

A more thorough treatment of this can be found in the worked example of testing a GUI, referenced in the Resources section.

XML

Beim Testen von Klassen, die XML verarbeiten, lohnt es sich, eine Routine zu schreiben, die zwei XML-DOMs auf Gleichheit vergleicht. Sie können dann das richtige DOM im Voraus programmgesteuert definieren und es mit der tatsächlichen Ausgabe Ihrer Verarbeitungsmethoden vergleichen.

Servlets

Mit Servlets können einige Ansätze funktionieren. Sie können ein Dummy-Servlet-Framework schreiben und es während eines Tests vorkonfigurieren. Das Framework muss Ableitungen von Klassen enthalten, die in der normalen Servlet-Umgebung gefunden werden. Diese Ableitungen sollten es Ihnen ermöglichen, ihre Antworten auf Methodenaufrufe vom Servlet vorkonfigurieren zu können.

Zum Beispiel: