Unit-Testing mit BlueJ
1. Testen
Testen bedeutet allgemein die Überprüfung, ob sich etwas so verhält, wie es spezifiziert ist oder erwartet wird.
Da jede Code-Änderung potentiell neue Fehler verursachen kann, ist häufiges und möglichst vollständiges Testen (Begriff Test-Abdeckung, EN: Coverage) extrem wichtig für die Software-Qualität.
Sehr viele Fehler lassen sich schon auf "niedrigstem Niveau" prüfen. Hier werden einzelne Methoden oder kleinstmögliche Gruppen von Methoden getestet – das Unit Testing.
Manche Fehler sind erst im Zusammenspiel größerer funktioneller Einheiten erkennbar – das Integration Testing
Prüfung auf Fehler bei der tatsächlichen Nutzung erfordern die Prüfung des Zusammenspiels des gesamten Systems – das System Testing
Bei der geringen Komplexität unserer Projekte ist Unit-Testing das einzige, das wir vorerst betreiben werden.
2. Konkreter Ablauf
Testen besteht aus dem "händischen" Konzipieren eines Testfalls, der bei vollständig festgelegten Umständen (üblicherweise Werte von Instanzvariablen, statischen Variablen, System Properties etc. aller beteiligten Objekte/Klassen) ein eindeutiges, "im Kopf" berechnetes und auch abfragbares Ergebnis liefert.
Dann wird die für den Testfall definierte Situation hergestellt und maschinell der vorab überlegte Erwartungswert mit dem tatsächlich vom Programm gelieferten verglichen.
Bei Übereinstimmung wird nichts getan, bei Diskrepanzen eine Fehlermeldung erzeugt. Somit ist kein Eingriff des Entwicklers nötig. Wenn alles passt, wird am Ende der gesamten Serie von Tests ein OK ausgegeben, anderenfalls Details zu allen gefundenen Fehlern.
3. JUnit5
Um solches Unit-Testen möglichst unkompliziert und zuverlässig durchgeführt werden kann, sind im Laufe der Zeit leistungsfähige "Frameworks" entwickelt worden. Das bekannteste dazu (und vor ca. 25 Jahren der Ausgangspunkt für das Konzept des Unit-Testing) ist JUnit, mittlerweile in Version 5.
In BlueJ 5 lässt sich eine Test-Klasse zu einer "normalen" Klasse wie folgt erzeugen:

Die erzeugte (noch weitgehend leere) Test-Klasse erscheit als grüner "Schatten" hinter der zu testenden Klasse.
Nun können wir mit Rechts-Klick auf die Test-Klasse eine Test-Methode erzeugen:

Die Test-Methode wurde erzeugt, links in Projekt-Fenster wird der Knopf "recording" rot und wir können wie gewohnt neue Objekte erzeugen, Methoden aufrufen, etc. Nach Methoden-Fertigstellung erscheint ein zuerst ein Dialog für die Test-Aktion:
man muss den Erwartungswert (und bei float oder double-Werten die geforderte Genauigkeit für die Übereinstimmung … Rundungsfehler, etc.!) angeben, gegen den das berechnete Ergebnis verglichen werden soll.
Danach öffnet sich ein leicht erweiterter Ergebnis-Dialog, den wir hier wegklicken können.

Wenn ein Fehler auftritt (hier wegen zu hoher Genauigkeitsforderung für die Factory-Methode ofArea(..)
), wird sowohl Erwartungswert als auch der berechnete Wert (hier blau markiert) ausgegeben (man sieht die überzogene Genauigkeit im letzten Parameter der letzten Test-Methode):

Nach Korrektur der Genauigkeit gelingt auch dieser Test, das Ergebnis ist somit:

3.1. Beispielklasse Circle
public class Circle
{
private float radius = 1.0f;
public Circle(float radius) {
setRadius(radius);
}
public void setRadius(float radius) {
if (radius <= 0) {
System.out.println("Invalid Radius: " + radius + " -> remains " + this.radius);
} else {
this.radius = radius;
}
}
public float calcDiameter() { // DE: Durchmesser
return 2*radius;
}
public float calcPerimeter() { // DE: Umfang (for circle also: circumference)
return (float) (Math.PI*calcDiameter()); // casting needed since PI is double
}
public float calcArea() { // DE: Flaeche
return (float) Math.PI * radius*radius;
}
// 2 Factory Methods (static!!):
public static Circle ofArea(float area) { // 'Factory method' to create obj. - static
float r = (float) Math.sqrt(area/Math.PI);
return new Circle(r); // behind the scene the usual way of real creation!!!!
}
public static Circle ofPerimeter(float perimeter) { // other factory method
float r = (float) (perimeter/2/Math.PI);
return new Circle(r); // see above
}
public float getRadius() {
return this.radius;
}
}
In der zu testenden Klasse werden unten 2 spezielle Methoden gezeigt:
public static Circle ofArea(float area)
und public static Circle ofPerimeter(float perimeter)
Sie sind statisch, da sie nicht an Objekten arbeiten, sondern Objekte erzeugen! Man nennt solche Methoden Factory-Methoden. Intern verwenden sie den Konstruktor, können aber verschiedenste Vorbereitungen treffen, wie hier die Berechnung des benötigten Radius aus Umfang bzw. Fläche.
Die Berechnung des Radius aus Fläche oder Umfang könnte auch sinnvoll als eigene Methoden implementiert und hier nur aufgerufen werden.
Auch diese Methoden sollten vermutlich eher statisch sein, da sie keinen Zugriff auf die Instanzvariablen haben/benötigen.
Die Erzeugung der Test-Methode testFactoryMethodOfArea1()
ist etwas komplizierter. Die Methode liefert ja ein Objekt zurück, wir wollen aber nur den Radius in diesem Objekt prüfen. Daher klicken wir das Häkchen bei _Assert that:_weg, dann wird keine Prüfung erzeugt. Im Ergebnis-Dialog wählen wir jedoch den Button get, wodurch das zurückgelieferte Objekt in der "Object Bench" abgelegt wird.
Somit können wir als letzte Aktion die Methode getRadius()
aufrufen, den hier nötigen Erwartungswert eingeben und erhalten die richtigen Inhalte der erstellten Test-Methode.
3.2. Genutzte einfache Test-Klasse
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* The test class CircleTest.
* @author renkin@spengergasse.at
* @version 2023-02-19
*/
public class CircleTest
{
/**
* Default constructor for test class CircleTest
*/
public CircleTest() { }
/**
* Sets up the test fixture.
* Called before every test case method.
*/
@BeforeEach
public void setUp() { }
/**
* Tears down the test fixture.
* Called after every test case method.
*/
@AfterEach
public void tearDown() { }
@Test
public void testCalcDiameter1() {
Circle circle1 = new Circle(2.0f);
assertEquals(4, circle1.calcDiameter(), 0.0001);
}
@Test
public void testCalcDiameter2() {
Circle circle1 = new Circle(0.5f);
assertEquals(1, circle1.calcDiameter(), 0.0001); // stops at first error
circle1.setRadius(1);
assertEquals(2, circle1.calcDiameter(), 0.01); // other problems not seen
// later we will see better ways to ensure all tests are done
}
@Test
public void testFactoryMethodOfArea1()
{
Circle circle1 = Circle.ofArea(12.566f);
assertEquals(2.0f, circle1.getRadius(), 0.0000001); // the relevant test
}
}
4. Links
Modultest – Wikipedia (DE)
und (inhaltlich wenig Überlappung!):
Unit testing - Wikipedia (EN)
Homepage JUnit 5