java\05_Testen_QS\ (Start mit java Aufgabe_TQ_NN_k.java)
| Nr. | Teststufe | Prüfgrundlage (V-Modell) |
|---|---|---|
| 1 | Komponententest (Unit-Test, Modultest) | Komponentenspezifikation (Modul-/Methodenentwurf; hier die Spezifikation der Methode beitrag in Anlage 3) |
| 2 | Integrationstest | technischer Systementwurf (Architektur, Schnittstellenbeschreibung der REST-API) |
| 3 | Systemtest | funktionaler Systementwurf (Pflichtenheft) |
| 4 | Abnahmetest | Anforderungsdefinition (Lastenheft, Vertrag) |
Beim statischen Test wird das Prüfobjekt nicht ausgeführt, sondern untersucht (gelesen, analysiert) – z. B. Code-Review des Pull-Requests zu zugangPruefen, statische Codeanalyse in der CI-Pipeline, Review des Pflichtenhefts. Beim dynamischen Test wird das Programm mit Testdaten ausgeführt und das tatsächliche mit dem erwarteten Ergebnis verglichen – z. B. Unit-Test der Methode beitrag, Lasttest des Check-in-Servers.
Erklärung: Die Teststufe ergibt sich aus der Größe des Testobjekts (eine Methode → mehrere zusammenarbeitende Teile → ganzes System → Einsatz beim Kunden). Situation 3 ist kein Abnahmetest, obwohl „alle Anforderungen“ geprüft werden: Es prüft der Hersteller in seiner Testumgebung, nicht der Kunde.
Beim Black-Box-Test werden die Testfälle nur aus der Spezifikation abgeleitet; der Quellcode wird nicht betrachtet. Geprüft wird das Ein-/Ausgabeverhalten (z. B. „aktives Mitglied ohne Premium will in die Sauna → ‚Sauna nur mit Premium‘“). Beim White-Box-Test werden die Testfälle aus dem Quellcode und seiner Kontrollstruktur abgeleitet, z. B. so, dass jeder Zweig von zugangPruefen einmal durchlaufen wird.
Vorteile des White-Box-Tests (zwei genügen):
Der Fehler hätte durch einen Regressionstest entdeckt werden können: Nach jeder Änderung (hier an der Beitragsberechnung) werden die bereits vorhandenen Testfälle – auch die für die Kursbuchung – erneut ausgeführt, um sicherzustellen, dass die Änderung keine unbeabsichtigten Auswirkungen auf bestehende, bereits abgenommene Funktionen hat.
Automatisieren, weil Regressionstests nach jeder Änderung vollständig wiederholt werden müssen: Manuell wäre das zu zeitaufwendig, teuer und fehleranfällig. Automatisiert laufen sie in der CI-Pipeline bei jedem Commit, schnell und immer unter gleichen Bedingungen.
Geeignet ist ein Lasttest (Performancetest; ergänzend Stresstest). Vorgehen: In einer produktionsnahen Testumgebung simuliert ein Lastgenerator (z. B. JMeter, Gatling) gleichzeitige Check-ins – zunächst die erwartete Spitzenlast von 50 Check-ins pro Minute über einen längeren Zeitraum, dann mit Reserve (z. B. das Doppelte). Gemessen werden Antwortzeit bis zur Öffnung des Drehkreuzes, Fehlerrate, CPU-, Speicher- und Datenbankauslastung; die Messwerte werden mit den Anforderungen (z. B. 95 % der Prüfungen unter 1 s) verglichen. Beim Stresstest wird die Last weiter gesteigert, bis das System an seine Grenze kommt, um zu sehen, ob es kontrolliert reagiert.
Erklärung: Der Klassiker aus S24 T2 A3a. Häufiger Fehler bei b): „Integrationstest“ – der prüft zwar das Zusammenspiel, entscheidend ist hier aber das Wiederholen bestehender Tests nach einer Änderung.
Ein Smoke-Test ist ein kurzer, oberflächlicher (meist automatisierter) Test der wichtigsten Grundfunktionen direkt nach einem Build bzw. Deployment. Er prüft, ob das System überhaupt lauffähig ist; schlägt er fehl, werden keine weiteren, aufwendigen Tests gestartet. Mögliche Prüfungen: Der Server startet und die REST-API antwortet (z. B. Status-Abfrage liefert HTTP 200) · Anmeldung mit einem Testkonto ist möglich · die Datenbankverbindung funktioniert und die Kursliste kann abgerufen werden · ein Test-QR-Code wird vom Zugangsdienst akzeptiert.
Die Aussage trifft nicht zu. Die Validierung gegen das JSON-Schema ist notwendig, aber nicht ausreichend: Ein Schema prüft nur die formale Struktur – ob Pflichtfelder vorhanden sind, ob Datentypen und Formate stimmen (Zahl, Datum). Nicht geprüft werden:
Fazit: Zusätzlich sind fachliche Tests mit bekannten Soll-Ergebnissen und ein Integrationstest mit der Gegenstelle nötig.
Erklärung: a) entspricht W23 T1 A1d, d) dem eRechnungs-Teil von W25 T1 A3b. Bei „beschreiben“ reicht ein Stichwort wie „Testumgebung“ nicht – es muss erkennbar sein, was dort festgelegt wird.
| Nr. | Fehlerart | Begründung |
|---|---|---|
| 1 | Syntaxfehler | Compiler meldet den Fehler, Programm wird nicht übersetzt |
| 2 | Laufzeitfehler | Index length existiert nicht → ArrayIndexOutOfBoundsException, Abbruch |
| 3 | Logikfehler | läuft, liefert aber eine falsche (negative) Summe |
| 4 | Logikfehler | läuft, liefert aber 20 % statt 80 % des Preises |
| 5 | Syntaxfehler | unbekannter Bezeichner, Compiler meldet den Fehler |
| 6 | Laufzeitfehler | Methodenaufruf auf null → NullPointerException |
Der Code ist syntaktisch korrekt, daher übersetzt ihn der Compiler; er führt auch keine verbotene Operation aus, daher bricht das Programm nicht ab. Er tut aber etwas anderes als beabsichtigt und liefert ein falsches Ergebnis. Solche Fehler findet man nur durch einen Vergleich mit dem erwarteten Ergebnis: Tests (Unit-Tests, Schreibtischtest) mit Testdaten, deren Soll-Ergebnis vorher aus der Spezifikation berechnet wurde, sowie Code-Reviews gegen die Spezifikation.
| i | puls[i] | Bedingung Zeile 05 (> 140) | summe | anzahl |
|---|---|---|---|---|
| 1 | 120 | falsch | 0 | 0 |
| 2 | 140 | falsch | 0 | 0 |
| 3 | 160 | wahr | 160 | 1 |
| 4 | 135 | falsch | 160 | 1 |
Tatsächliches Ergebnis: 160 / 5 = 32.0. Erwartetes Ergebnis: Werte ≥ 140 sind 150, 140 und 160 → (150 + 140 + 160) / 3 = 150.0.
| Zeile | Fehler und Auswirkung |
|---|---|
| 04 | Die Schleife beginnt bei Index 1 → der erste Pulswert wird nie ausgewertet (hier fehlt 150). |
| 05 | > statt >= → Werte genau auf der Grenze werden nicht gezählt, obwohl „mindestens“ gefordert ist (hier fehlt 140). |
| 13 | Division durch puls.length statt durch anzahl → es wird durch die Anzahl aller Werte geteilt, der Durchschnitt ist viel zu klein. |
04 for (int i = 0; i < puls.length; i++) {
05 if (puls[i] >= grenze) {
13 return (double) summe / anzahl;
Z. B. puls = {100, 110}, grenze = 140: Kein Wert liegt im Belastungsbereich, beide Fassungen liefern 0.0. (Auch das leere Array liefert in beiden Fassungen 0.0.) Folgerung: Ein bestandener Test beweist nicht, dass der Code fehlerfrei ist. Testdaten müssen gezielt so gewählt werden, dass Fehler sichtbar werden können – Grenzwerte, erstes und letztes Element, mehrere Werte im Bereich.
JUnit 5 – Tests für die korrigierte Methode (Klasse Trainingsauswertung)
class TrainingsauswertungTest {
private final Trainingsauswertung t = new Trainingsauswertung();
@Test
void durchschnittAllerWerteAbGrenze() {
assertEquals(150.0, t.belastungsDurchschnitt(new int[]{150, 120, 140, 160, 135}, 140), 0.001);
}
@Test
void wertGenauAufDerGrenzeZaehlt() {
assertEquals(140.0, t.belastungsDurchschnitt(new int[]{140}, 140), 0.001);
}
@Test
void keinWertImBelastungsbereichLiefertNull() {
assertEquals(0.0, t.belastungsDurchschnitt(new int[]{100, 110}, 140), 0.001);
}
}
Ausgabe von Aufgabe_TQ_02_2.java (Auszug)
Tests gegen die FEHLERHAFTE Methode: FEHLER Testdaten der Aufgabe erwartet 150,0, tatsächlich 32,0 OK kein Wert im Belastungsbereich erwartet 0,0, tatsächlich 0,0 FEHLER ein Element, genau auf der Grenze erwartet 140,0, tatsächlich 0,0 FEHLER alle Werte im Bereich erwartet 155,0, tatsächlich 80,0 OK leeres Array erwartet 0,0, tatsächlich 0,0 -> 2 bestanden, 3 fehlgeschlagen Tests gegen die KORRIGIERTE Methode: ... -> 5 bestanden, 0 fehlgeschlagen
i <= puls.length - 1 gleichwertig. d) 1 P passende Testdaten, 1 P Folgerung.Erklärung: Die drei Fehler verstärken sich hier gegenseitig. Typisch für Prüfungsaufgaben: Start bei 1 ist nur dann richtig, wenn das Element 0 vorher bereits verarbeitet wurde (z. B. als Startwert beim Minimum) – das ist hier nicht der Fall.
| Typ | Variable | Werteverlauf |
|---|---|---|
| integer | i | 0, 1, 2, 3, 4, 5, 6 |
| string | kurs | Aqua-Fitness, Rückenfit, Yoga |
| integer | anzahl | 0, 1, 2, 0, 1, 2, 3, 0, 1, 2 |
| integer | summe | 0, 14, 32, 0, 10, 22, 30, 0, 16, 29 |
| integer | max | 0, 14, 18 (bleibt bis zum Ende 18) |
| integer | anzahlGesamt | 0, 2, 5 |
| integer | summeGesamt | 0, 32, 62 |
Tatsächliche Ausgabe:
Kursstatistik Aqua-Fitness: 2 Termine, max. 18, Ø 16.0 Rückenfit: 3 Termine, max. 18, Ø 10.0 Gesamt: 5 Termine, Ø 31.0
| Zeile | Fehler (Ursache) | Auswirkung auf die Ausgabe |
|---|---|---|
| nach 18 (Gruppenwechsel) | max wird beim Gruppenwechsel nicht zurückgesetzt – nur anzahl und summe. | Das Maximum „wandert“ in den nächsten Kurs: Rückenfit zeigt max. 18 (von Aqua-Fitness) statt 12. |
| nach 25 (Schleifenende) | Die letzte Gruppe wird nicht ausgewertet: Ausgabe und Aufsummierung erfolgen nur beim Wechsel, der letzte Kurs erlebt aber keinen Wechsel mehr. | Die Zeile für Yoga fehlt; die Gesamtwerte enthalten Yoga nicht (5 statt 7 Termine). |
| 27 | Division durch anzahl (Termine des letzten Kurses) statt durch anzahlGesamt. | Gesamtdurchschnitt 62 / 2 = 31.0 statt 13.0. |
nach Zeile 18 einfügen: 18a max := 0 nach Zeile 25 einfügen (letzte Gruppe auswerten): 25a ausgabe(kurs + ": " + anzahl + " Termine, max. " + max 25b + ", Ø " + summe / anzahl) 25c anzahlGesamt := anzahlGesamt + anzahl 25d summeGesamt := summeGesamt + summe Zeile 27 ersetzen durch: 27 + summeGesamt / anzahlGesamt)
Vollständige korrigierte Prozedur (inklusive Teil d):
prozedur kursStatistik(termine : List<Termin>)
ausgabe("Kursstatistik")
wenn termine.size() = 0 dann // d)
ausgabe("Keine Termine vorhanden")
rückgabe
Ende wenn
kurs : string := termine.get(0).getKurs()
anzahl : integer := 0
summe : integer := 0
max : integer := 0
anzahlGesamt : integer := 0
summeGesamt : integer := 0
zähle i von 0 bis termine.size() - 1
t : Termin := termine.get(i)
wenn t.getKurs() <> kurs dann
ausgabe(kurs + ": " + anzahl + " Termine, max. " + max
+ ", Ø " + summe / anzahl)
anzahlGesamt := anzahlGesamt + anzahl
summeGesamt := summeGesamt + summe
kurs := t.getKurs()
anzahl := 0
summe := 0
max := 0 // Korrektur 1
Ende wenn
anzahl := anzahl + 1
summe := summe + t.getTeilnehmer()
wenn t.getTeilnehmer() > max dann
max := t.getTeilnehmer()
Ende wenn
Ende zähle
// Korrektur 2: letzte Gruppe auswerten
ausgabe(kurs + ": " + anzahl + " Termine, max. " + max
+ ", Ø " + summe / anzahl)
anzahlGesamt := anzahlGesamt + anzahl
summeGesamt := summeGesamt + summe
// Korrektur 3: durch die Gesamtanzahl teilen
ausgabe("Gesamt: " + anzahlGesamt + " Termine, Ø "
+ summeGesamt / anzahlGesamt)
Ende prozedur
In Zeile 03 greift termine.get(0) auf ein Element zu, das nicht existiert → Laufzeitfehler (Indexüberschreitung, in Java IndexOutOfBoundsException), das Programm bricht nach der Überschrift ab. (Ohne diesen Abbruch würde später durch 0 geteilt.) Ergänzung nach Zeile 02:
02a wenn termine.size() = 0 dann
02b ausgabe("Keine Termine vorhanden")
02c rückgabe
02d Ende wenn
Ebenfalls akzeptiert: zusätzliche Grenzfalltests (leere Liste, nur ein Kurs, ein einziger Termin) als Unit-Tests.
Java · Aufgabe_TQ_02_3.java (korrigierte Fassung, Auszug)
static void kursStatistik(List<Termin> termine) {
ausgabe("Kursstatistik");
if (termine.size() == 0) { // d) leere Liste abfangen
ausgabe("Keine Termine vorhanden");
return;
}
String kurs = termine.get(0).getKurs();
int anzahl = 0;
int summe = 0;
int max = 0;
int anzahlGesamt = 0;
int summeGesamt = 0;
for (int i = 0; i <= termine.size() - 1; i++) {
Termin t = termine.get(i);
if (!t.getKurs().equals(kurs)) {
ausgabe(kurs + ": " + anzahl + " Termine, max. " + max
+ ", Ø " + (double) summe / anzahl);
anzahlGesamt = anzahlGesamt + anzahl;
summeGesamt = summeGesamt + summe;
kurs = t.getKurs();
anzahl = 0;
summe = 0;
max = 0; // Korrektur 1
}
anzahl = anzahl + 1;
summe = summe + t.getTeilnehmer();
if (t.getTeilnehmer() > max) {
max = t.getTeilnehmer();
}
}
// Korrektur 2 (nach Zeile 25): letzte Gruppe ausgeben und aufsummieren
ausgabe(kurs + ": " + anzahl + " Termine, max. " + max + ", Ø " + (double) summe / anzahl);
anzahlGesamt = anzahlGesamt + anzahl;
summeGesamt = summeGesamt + summe;
// Korrektur 3 (Zeile 27): durch anzahlGesamt teilen
ausgabe("Gesamt: " + anzahlGesamt + " Termine, Ø " + (double) summeGesamt / anzahlGesamt);
}
Ausgabe von Aufgabe_TQ_02_3.java (Auszug)
a) Ausgabe der FEHLERHAFTEN Prozedur (mit Variablenstand nach jedem Durchlauf):
Kursstatistik
[i=0] kurs=Aqua-Fitness anzahl=1 summe=14 max=14 anzahlGesamt=0 summeGesamt=0
[i=1] kurs=Aqua-Fitness anzahl=2 summe=32 max=18 anzahlGesamt=0 summeGesamt=0
Aqua-Fitness: 2 Termine, max. 18, Ø 16.0
[i=2] kurs=Rückenfit anzahl=1 summe=10 max=18 anzahlGesamt=2 summeGesamt=32
[i=3] kurs=Rückenfit anzahl=2 summe=22 max=18 anzahlGesamt=2 summeGesamt=32
[i=4] kurs=Rückenfit anzahl=3 summe=30 max=18 anzahlGesamt=2 summeGesamt=32
Rückenfit: 3 Termine, max. 18, Ø 10.0
[i=5] kurs=Yoga anzahl=1 summe=16 max=18 anzahlGesamt=5 summeGesamt=62
[i=6] kurs=Yoga anzahl=2 summe=29 max=18 anzahlGesamt=5 summeGesamt=62
Gesamt: 5 Termine, Ø 31.0
stimmt mit erwarteter Ausgabe überein? false
c) Ausgabe der KORRIGIERTEN Prozedur:
Kursstatistik
Aqua-Fitness: 2 Termine, max. 18, Ø 16.0
Rückenfit: 3 Termine, max. 12, Ø 10.0
Yoga: 2 Termine, max. 16, Ø 14.5
Gesamt: 7 Termine, Ø 13.0
stimmt mit erwarteter Ausgabe überein? true
d) Aufruf mit leerer Liste:
Kursstatistik
fehlerhaft: Laufzeitfehler IndexOutOfBoundsException: Index 0 out of bounds for length 0
korrigiert:
Kursstatistik
Keine Termine vorhanden
max := 0 2 P; Auswertung der letzten Gruppe nach der Schleife 4 P (Ausgabe 2 P, Fortschreiben beider Gesamtwerte 2 P); Divisor 2 P. Gleichwertig: Ausgabe in eine Hilfsprozedur gruppeAusgeben(…) auslagern (DRY), max beim Wechsel mit der Teilnehmerzahl des neuen Termins initialisieren. d) 1 P Beschreibung, 2 P Ergänzung. e) je Testart mit Begründung 1 P.Erklärung: Typischer Gruppenwechsel wie in S23 T2 A3 (dort: Ticketstatistik je Monat). Die Ausgabe der letzten Gruppe nach der Schleife wird praktisch immer vergessen. Achtung: Der Fehler „max wird nicht zurückgesetzt“ fällt nur auf, wenn ein späterer Kurs ein kleineres Maximum hat – deshalb sind die Testdaten so gewählt.
Ein Unit-Test ist ein automatisierter Test der kleinsten testbaren Einheit eines Programms (z. B. der Methode buchen der Klasse Kurs), die isoliert vom Rest des Systems mit festgelegten Eingaben aufgerufen wird; das tatsächliche Ergebnis wird automatisch mit dem erwarteten verglichen. Er wird vom Entwickler geschrieben und dient dazu, Fehler früh und genau lokalisiert zu finden und bei jeder Änderung automatisch erneut zu prüfen, dass die Einheit noch korrekt arbeitet (Regressionstest, Absicherung beim Refactoring).
| Zeile | Phase | Aufgabe |
|---|---|---|
| (1) | Arrange | Ausgangszustand herstellen: Testobjekt anlegen (Kurs „Yoga“ mit 12 Plätzen, 0 gebucht). |
| (2) | Act | Die zu testende Aktion ausführen: 3 Plätze buchen. |
| (3) | Assert | Tatsächliches Ergebnis mit dem erwarteten vergleichen: Es müssen 9 freie Plätze übrig sein. |
Ausgabe von Aufgabe_TQ_03_1.java
OK buchungVerringertFreiePlaetze OK ueberbuchungWirdAbgelehnt
| Testfall | Aufruf | Rückgabewert | erwartet | Ergebnis |
|---|---|---|---|---|
| 1 | freiePlaetze(20, 15) | 5 | 5 | OK |
| 2 | freiePlaetze(20, 0) | −1 (gebucht > 0 falsch) | 20 | Fehler |
| 3 | freiePlaetze(20, 20) | −1 (20 < 20 falsch) | 0 | Fehler |
| 4 | freiePlaetze(20, 21) | −1 | −1 | OK |
| 5 | freiePlaetze(0, 0) | −1 | −1 | OK |
| 6 | freiePlaetze(20, 1) | 19 | 19 | OK |
01 public int freiePlaetze(int kapazitaet, int gebucht) {
02 int frei = -1;
03 if (kapazitaet > 0 && gebucht >= 0) { // 0 Buchungen sind zulässig
04 if (gebucht <= kapazitaet) { // ein voll belegter Kurs ist zulässig
05 frei = kapazitaet - gebucht;
06 }
07 }
08 return frei;
09 }
Ursache: Alle sechs Prüfungen stehen in einer Testmethode. Schlägt ein assertEquals fehl, wird ein AssertionError ausgelöst und die Testmethode an dieser Stelle abgebrochen. Testfall 2 schlägt fehl, die Testfälle 3 bis 6 werden gar nicht mehr ausgeführt – der zweite Fehler (Testfall 3) bleibt deshalb zunächst unbemerkt.
Besser: je Testfall eine eigene Testmethode mit sprechendem Namen, oder ein parametrisierter Test, bei dem jede Datenzeile als eigener Testfall ausgeführt und gemeldet wird (alternativ assertAll):
@ParameterizedTest
@CsvSource({
"20, 15, 5", // normaler Fall
"20, 0, 20", // Grenzwert: nichts gebucht
"20, 20, 0", // Grenzwert: voll belegt
"20, 21, -1", // ungültig: überbucht
" 0, 0, -1", // ungültig: keine Kapazität
"20, 1, 19"
})
void freiePlaetzeLiefertErwartetenWert(int kapazitaet, int gebucht, int erwartet) {
assertEquals(erwartet, freiePlaetze(kapazitaet, gebucht));
}
Ausgabe von Aufgabe_TQ_03_2.java (Auszug)
a) Jeder Testfall einzeln gegen die FEHLERHAFTE Methode:
TF | Aufruf | erwartet | tatsächlich | Ergebnis
1 | freiePlaetze(20, 15) | 5 | 5 | OK
2 | freiePlaetze(20, 0) | 20 | -1 | Fehler
3 | freiePlaetze(20, 20) | 0 | -1 | Fehler
4 | freiePlaetze(20, 21) | -1 | -1 | OK
5 | freiePlaetze( 0, 0) | -1 | -1 | OK
6 | freiePlaetze(20, 1) | 19 | 19 | OK
c) Eine einzige JUnit-Testmethode mit allen sechs asserts (fehlerhafte Methode):
Testmethode FEHLGESCHLAGEN in Testfall 2: expected: <20> but was: <-1>
(Testfälle 3 bis 6 wurden nicht mehr ausgeführt)
...
JUnit-Lauf korrigiert:
Testmethode bestanden
if (kapazitaet > 0 && gebucht >= 0 && gebucht <= kapazitaet). c) 1,5 P Ursache (Abbruch beim ersten fehlgeschlagenen assert), 1,5 P Verbesserung.Erklärung: Wie S22 T2 A2b: Beide Fehler sitzen an Grenzwerten (0 und kapazitaet). Die Testfälle 2 und 3 sind genau die Grenzwerttests, die solche Fehler aufdecken.
Anweisungsüberdeckung (statement coverage): Maß dafür, wie viele der ausführbaren Anweisungen durch die Testfälle mindestens einmal ausgeführt werden (Angabe in Prozent). Bei 100 % wurde jede Anweisung mindestens einmal ausgeführt – aber nicht unbedingt jedes Ergebnis jeder Entscheidung: Ein if ohne else ist schon mit einem Testfall vollständig überdeckt, in dem die Bedingung wahr ist. Schwächstes der drei Maße.
Zweigüberdeckung (branch/decision coverage): Jedes mögliche Ergebnis jeder Entscheidung – wahr und falsch – muss mindestens einmal durchlaufen werden, also auch der „leere“ falsch-Zweig eines if ohne else. Strenger als die Anweisungsüberdeckung: Vollständige Zweigüberdeckung schließt vollständige Anweisungsüberdeckung ein, aber nicht umgekehrt.
Pfadüberdeckung (path coverage): Jeder mögliche Pfad – eine Folge von Anweisungen vom Eintrittspunkt der Methode bis zu einem Austrittspunkt – muss mindestens einmal ausgeführt werden. Stärkstes Maß, schließt die vollständige Zweigüberdeckung ein. Die Zahl der Pfade wächst sehr schnell (jede unabhängige Entscheidung verdoppelt sie, Schleifen erzeugen theoretisch unbegrenzt viele Pfade); vollständige Pfadüberdeckung ist daher nur bei kleinen Methoden bzw. mit begrenzter Zahl an Schleifendurchläufen praktikabel.
zugangPruefen (B-Knoten hinterlegt)Anweisungsüberdeckung: 3 Testfälle. A5 wird nur bei inaktivem Vertrag ausgeführt (dann keine andere A-Anweisung), A2 und A3 schließen sich gegenseitig aus (dann/sonst von B3). Also mindestens drei; A1 und A4 lassen sich mitnehmen, z. B. (aktiv, Premium, offener Betrag, Sauna), (aktiv, kein Premium, offener Betrag, Sauna), (nicht aktiv).
Zweigüberdeckung: 4 Testfälle. Zusätzlich muss jede Bedingung auch falsch werden; insbesondere der falsch-Zweig von B2 (Bereich ist nicht die Sauna) kommt in keinem der drei Testfälle oben vor und lässt sich mit A2/A3 nicht kombinieren.
| Nr. | Vertrag aktiv | Premium | offener Betrag | Bereich | durchlaufene Zweige | Rückgabe |
|---|---|---|---|---|---|---|
| T1 | nein | beliebig | beliebig | beliebig | B1 f | „Kein Zutritt“ |
| T2 | ja | ja | 25,50 € | Sauna | B1 w, B2 w, B3 w, B4 w | „Zutritt Sauna (Zahlung offen)“ |
| T3 | ja | nein | 0,00 € | Sauna | B1 w, B2 w, B3 f, B4 f | „Sauna nur mit Premium“ |
| T4 | ja | beliebig | beliebig | Trainingsbereich | B1 w, B2 f, B4 w oder f | „Zutritt Trainingsbereich …“ |
| Nummer | Pfad | Beispiel |
|---|---|---|
| 1 | B1, A5 | Vertrag nicht aktiv |
| 2 | B1, A1, B2, B3, A2, B4, A4 | Sauna, Premium, Zahlung offen |
| 3 | B1, A1, B2, B3, A2, B4 | Sauna, Premium, nichts offen |
| 4 | B1, A1, B2, B3, A3, B4, A4 | Sauna, kein Premium, Zahlung offen |
| 5 | B1, A1, B2, B3, A3, B4 | Sauna, kein Premium, nichts offen |
| 6 | B1, A1, B2, B4, A4 | Trainingsbereich, Zahlung offen |
| 7 | B1, A1, B2, B4 | Trainingsbereich, nichts offen |
Die Reihenfolge der Pfade ist beliebig. Zählkontrolle: falls B1 falsch 1 Pfad; falls B1 wahr: (B2 falsch: 1 + B2 wahr: 2) = 3 Möglichkeiten, jeweils mal 2 für B4 → 6; zusammen 7.
Überdeckung zeigt nur, welcher Code ausgeführt wurde, nicht, ob das Ergebnis mit sinnvollen Assertions geprüft wurde, und sie kann fehlende Funktionalität nicht aufdecken. Beispiele zu zugangPruefen: Ein Test, der die Methode aufruft, die zurückgegebene Meldung aber nicht prüft, zählt für die Überdeckung voll mit – ein Tippfehler in „Zutritt Sauna“ (das Drehkreuz öffnet dann nicht) bliebe unentdeckt. Oder: Wäre in der Spezifikation gefordert, dass gesperrte Mitglieder trotz aktiven Vertrags abgewiesen werden, fehlte diese Prüfung im Code ganz – kein Überdeckungsmaß zeigt Code an, der gar nicht existiert. Auch ein Aufruf mit bereich = null führt trotz 100 % Pfadüberdeckung zu einer NullPointerException.
Ausgabe von Aufgabe_TQ_03_3.java (instrumentierte Methode, alle Eingabekombinationen ausprobiert)
d) Alle Pfade (7):
1 B1, A5 -> "Kein Zutritt"
z. B. aktiv=false premium=true offen=25,50 bereich=Sauna
2 B1, A1, B2, B3, A2, B4, A4 -> "Zutritt Sauna (Zahlung offen)"
z. B. aktiv=true premium=true offen=25,50 bereich=Sauna
3 B1, A1, B2, B3, A2, B4 -> "Zutritt Sauna"
z. B. aktiv=true premium=true offen= 0,00 bereich=Sauna
4 B1, A1, B2, B3, A3, B4, A4 -> "Sauna nur mit Premium (Zahlung offen)"
z. B. aktiv=true premium=false offen=25,50 bereich=Sauna
5 B1, A1, B2, B3, A3, B4 -> "Sauna nur mit Premium"
z. B. aktiv=true premium=false offen= 0,00 bereich=Sauna
6 B1, A1, B2, B4, A4 -> "Zutritt Trainingsbereich (Zahlung offen)"
z. B. aktiv=true premium=true offen=25,50 bereich=Trainingsbereich
7 B1, A1, B2, B4 -> "Zutritt Trainingsbereich"
z. B. aktiv=true premium=true offen= 0,00 bereich=Trainingsbereich
c) Zweige insgesamt: 8 [B1 falsch, B1 wahr, B2 falsch, B2 wahr, B3 falsch, B3 wahr, B4 falsch, B4 wahr]
Minimale Anzahl Testfälle Anweisungsüberdeckung (C0): 3
Pfad 1: B1, A5
Pfad 2: B1, A1, B2, B3, A2, B4, A4
Pfad 4: B1, A1, B2, B3, A3, B4, A4
Minimale Anzahl Testfälle Zweigüberdeckung (C1): 4
Pfad 1: B1, A5
Pfad 3: B1, A1, B2, B3, A2, B4
Pfad 4: B1, A1, B2, B3, A3, B4, A4
Pfad 6: B1, A1, B2, B4, A4
Pfadüberdeckung (C2): 7 Testfälle
...
T1-T3 decken alle Anweisungen ab: true
T1-T3 lassen diese Zweige aus: [B2 falsch]
T1-T4 decken alle Zweige ab: true
e) Trotz 100 % Pfadüberdeckung: Aufruf mit bereich = null (z. B. fehlender Wert aus der App)
-> NullPointerException in bereich.equals(...) - kein Überdeckungsmaß hat das angezeigt
Erklärung: Wie S24 T2 A3b/c (dort Login mit B1–B3 und A1–A4, fünf Pfade). Merke den Unterschied der drei Zahlen: 3 (C0) < 4 (C1) < 7 (C2). Die zyklomatische Zahl nach McCabe ist hier 4 Entscheidungen + 1 = 5 – eine obere Grenze für die Testfälle der Zweigüberdeckung (4 ≤ 5).
| Nr. | Qualitätsmerkmal | Untermerkmal (nicht gefordert) |
|---|---|---|
| 1 | Leistungseffizienz | Zeitverhalten |
| 2 | Zuverlässigkeit | Fehlertoleranz (auch Verfügbarkeit) |
| 3 | Benutzbarkeit | Erlernbarkeit |
| 4 | Kompatibilität | Interoperabilität |
| 5 | Wartbarkeit | Modularität, Modifizierbarkeit |
| 6 | Sicherheit | Vertraulichkeit |
Erklärung: Nr. 2 ist keine Leistungs-, sondern eine Zuverlässigkeitsanforderung: Es geht darum, dass das System auch bei einer Störung weiterarbeitet. Nr. 6 ist Sicherheit im Sinne von Informationssicherheit (Schutz vertraulicher Gesundheitsdaten).
Zuverlässigkeit ist die Fähigkeit der Software, ihre Funktionen unter festgelegten Bedingungen über einen festgelegten Zeitraum dauerhaft und auch bei Störungen zu erbringen. Untermerkmale am Beispiel des Check-ins (zwei genügen):
Wartbarkeit beschreibt, wie leicht und mit welchem Aufwand die Software geändert werden kann – um Fehler zu beheben, sie zu erweitern oder an neue Anforderungen anzupassen. Untermerkmale: Modularität, Wiederverwendbarkeit, Analysierbarkeit, Modifizierbarkeit, Testbarkeit (zwei genügen). Für Brückner Digital ist sie besonders wichtig, weil VitaPark Connect über Jahre betreut und weiterentwickelt wird: neue Tarife und Studios, weitere Armbandhersteller, Änderungen der GesundPlus-Spezifikation. Gut wartbarer Code senkt die Kosten jeder Änderung und das Risiko, dabei neue Fehler einzubauen.
Funktionalität · Zuverlässigkeit · Benutzbarkeit · Effizienz · Änderbarkeit (Wartbarkeit) · Übertragbarkeit
Sicherheit (Informationssicherheit; in 9126 Untermerkmal der Funktionalität) und Kompatibilität (fasst Interoperabilität und Koexistenz zusammen).
| Merkmal | konkrete Anforderung | messbares Abnahmekriterium | Prüfung |
|---|---|---|---|
| Zuverlässigkeit | Der Check-in ist während der Öffnungszeiten verfügbar und funktioniert auch bei Ausfall der Serververbindung. | Verfügbarkeit ≥ 99,9 % zwischen 6 und 23 Uhr pro Monat; bei Verbindungsabbruch werden gültige Karten der letzten 24 h offline akzeptiert; nach einem Neustart ist der Dienst in ≤ 5 min wieder bereit. | Ausfall-/Fehlertoleranztest im Systemtest (Server stoppen, Netzwerk trennen), Langzeittest, Messung im Pilotbetrieb (Monitoring) |
| Leistungseffizienz | Das Drehkreuz öffnet auch zur Spitzenzeit ohne spürbare Wartezeit. | Bei 50 Check-ins pro Minute (Reserve: 100) öffnet das Drehkreuz in 95 % der Fälle spätestens 1 s nach dem Scan; Server-CPU-Last ≤ 70 %. | Lasttest mit Lastgenerator (simulierte Scans), Messung der Antwortzeiten; Stresstest über die Spitzenlast hinaus |
| Sicherheit | Nur aktive Mitglieder erhalten Zutritt; ein kopierter QR-Code (Screenshot) darf nicht funktionieren; Übertragung verschlüsselt. | Der QR-Code enthält ein signiertes Token, das höchstens 30 s gültig ist; abgelaufene, manipulierte oder fremde Codes werden in 100 % der Testfälle abgewiesen; Kommunikation nur über TLS; keine kritischen Befunde im Penetrationstest. | Sicherheitstest/Penetrationstest, Negativtests mit manipulierten und abgelaufenen Codes, Code-Review der Zugangsprüfung |
Erklärung: S23 T1 A2a fragte tatsächlich nach ISO 9126. Die Tabelle in c) verbindet TQ-04 mit TQ-01: Zu jeder Qualitätsanforderung gehört eine Prüfung – nicht-funktionale Anforderungen werden durch nicht-funktionale Tests (Last-, Ausfall-, Sicherheitstest) abgenommen.
Ein Schreibtischtest ist das manuelle Durchspielen eines Algorithmus mit konkreten Testdaten ohne Computer; nach jedem Schritt werden die Werte der Variablen in einer Tabelle notiert, um das Ergebnis zu ermitteln und Fehler zu finden.
| i | minuten[i] | Bedingung Zeile 04 | Bedingung Zeile 07 | punkte |
|---|---|---|---|---|
| – | – | – | – | 0 |
| 0 | 45 | falsch | wahr | 1 |
| 1 | 70 | wahr | – | 4 |
| 2 | 20 | falsch | falsch | 4 |
| 3 | 60 | wahr | – | 7 |
| 4 | 30 | falsch | wahr | 8 |
Rückgabewert 8. Das entspricht der Regel in Anlage 3: 45 → 1, 70 → 3, 20 → 0, 60 → 3 (ab 60 Minuten), 30 → 1 (ab 30 Minuten); 1 + 3 + 0 + 3 + 1 = 8.
Ausgabe von Aufgabe_TQ_05_1.java
Schreibtischtest bonuspunkte([45, 70, 20, 60, 30]): i | minuten[i] | >= 60 (Z. 04) | >= 30 (Z. 07) | punkte - | - | - | - | 0 0 | 45 | falsch | wahr | 1 1 | 70 | wahr | - | 4 2 | 20 | falsch | falsch | 4 3 | 60 | wahr | - | 7 4 | 30 | falsch | wahr | 8 Rückgabewert: 8 Kontrolle Grenzwerte: [59] -> 1, [29] -> 0, [] -> 0
Erklärung: Die Bedingung in Zeile 07 wird nur geprüft, wenn die in Zeile 04 falsch ist (sonst-Zweig). 60 und 30 sind Grenzwerte – „ab“ heißt >=, die Funktion ist hier korrekt.
| tag | std | besucher[tag][std] | Bedingung Zeile 06 (> 80) | folge | anzahl |
|---|---|---|---|---|---|
| – | – | – | – | 0 | 0 |
| 0 | 0 | 40 | falsch | 0 | 0 |
| 0 | 1 | 85 | wahr | 1 | 0 |
| 0 | 2 | 90 | wahr | 2 | 1 |
| 0 | 3 | 95 | wahr | 3 | 1 |
| 1 | 0 | 70 | falsch | 0 | 1 |
| 1 | 1 | 82 | wahr | 1 | 1 |
| 1 | 2 | 60 | falsch | 0 | 1 |
| 1 | 3 | 88 | wahr | 1 | 1 |
| 2 | 0 | 91 | wahr | 2 | 2 |
| 2 | 1 | 50 | falsch | 0 | 2 |
| 2 | 2 | 96 | wahr | 1 | 2 |
| 2 | 3 | 97 | wahr | 2 | 3 |
Tatsächliches Ergebnis: 3. Erwartet laut Spezifikation: 2 (Montag 18–20 Uhr und Mittwoch 19–20 Uhr). Abweichung: Am Mittwoch um 17 Uhr (91) wird eine Spitzenphase gezählt, weil folge vom Dienstag 20 Uhr (88) noch auf 1 stand. Die Folge wird also über die Tagesgrenze (die Nacht) hinweg fortgesetzt, was die Spezifikation ausschließt.
Zeile 03: folge := 0 wird nur einmal vor der äußeren Schleife ausgeführt, statt zu Beginn jedes Tages. Korrektur: Zeile 03 in die äußere Schleife verschieben (nach Zeile 04, vor die innere Schleife).
01 funktion spitzenphasen(besucher : integer[][], grenze : integer) : integer 02 anzahl : integer := 0 04 zähle tag von 0 bis besucher.length - 1 03 folge : integer := 0 // jetzt für jeden Tag neu 05 zähle std von 0 bis besucher[tag].length - 1 06 wenn besucher[tag][std] > grenze dann 07 folge := folge + 1 08 wenn folge = 2 dann 09 anzahl := anzahl + 1 10 Ende wenn 11 sonst 12 folge := 0 13 Ende wenn 14 Ende zähle 15 Ende zähle 16 rückgabe anzahl 17 Ende funktion
Ausgabe von Aufgabe_TQ_05_2.java (Auszug)
tatsächliches Ergebnis: 3 b) erwartetes Ergebnis laut Spezifikation (korrigierte Funktion): 2 Zusatz: Dienstag endet unter der Grenze -> fehlerhaft 2, korrigiert 2 (Fehler bleibt mit solchen Testdaten unentdeckt)
folge := 0 zusätzlich als erste Anweisung in der äußeren Schleife).Erklärung: Klassischer Fehler bei verschachtelten Schleifen: Initialisierung in der falschen Schleife. Er zeigt sich nur, wenn ein Tag mit einem Wert über der Grenze endet und der nächste mit einem beginnt – der Zusatzlauf zeigt, dass andere Testdaten ihn nicht finden.
| TF | Testdaten | erwartetes Ergebnis | tatsächliches Ergebnis |
|---|---|---|---|
| 1 | {W, W, F, W, W, W, F} | 3 | 3 |
| 2 | {W, F, W, W, W, W} | 4 | 1 |
| 3 | {F, F, F} | 0 | 0 |
| 4 | {W, W, W} | 3 | 0 |
| 5 | {} | IllegalArgumentException | 0 |
| 6 | null | IllegalArgumentException | IllegalArgumentException |
| Aufruf | i | i >= t.length | t[i] | Rückgabewert |
|---|---|---|---|---|
| 1 | 3 | falsch | W (true) | 1 + Ergebnis Aufruf 2 = 3 |
| 2 | 4 | falsch | W (true) | 1 + Ergebnis Aufruf 3 = 2 |
| 3 | 5 | falsch | W (true) | 1 + Ergebnis Aufruf 4 = 1 |
| 4 | 6 | wahr | nicht ausgewertet | 0 (Basisfall) |
Ergebnis: serieAb(t, 3) = 3. Im 4. Aufruf ist i = 6 = t.length; weil der linke Teil der oder-Bedingung schon wahr ist, wird t[6] nicht mehr ausgewertet – sonst gäbe es eine Indexüberschreitung.
max wird nur aktualisiert, wenn eine Serie durch einen Ruhetag (false) beendet wird. Endet das Array mit Trainingstagen, wird die letzte Serie nie ausgewertet (TF2 liefert 1 statt 4, TF4 liefert 0 statt 3). Korrektur: max direkt nach serie++ aktualisieren – oder nach der Schleife (nach Zeile 16) zusätzlich if (serie > max) { max = serie; } einfügen.IllegalArgumentException ausgelöst werden (TF5 liefert 0). Korrektur: if (trainiert == null || trainiert.length == 0) {public int laengsteSerie(boolean[] trainiert) {
if (trainiert == null || trainiert.length == 0) { // Fehler 2 behoben
throw new IllegalArgumentException("keine Daten");
}
int serie = 0;
int max = 0;
for (int i = 0; i < trainiert.length; i++) {
if (trainiert[i]) {
serie++;
if (serie > max) { // Fehler 1 behoben:
max = serie; // Maximum nach jedem Trainingstag
}
} else {
serie = 0;
}
}
return max;
}
Es genügen 2 Testfälle: TF6 und TF1 (statt TF1 auch TF2).
throw) wird nur bei null ausgeführt → TF6 ist nötig; er führt aber nur die Zeilen 02 und 03 aus.serie > max wahr ist (Zeilen 11, 12, 14), und die Rückgabe (Zeile 17). Das leistet TF1 (ebenso TF2). TF3 erreicht Zeile 09 und 12 nicht, TF4 die Zeilen 11, 12, 14 nicht, TF5 nicht einmal den Schleifenrumpf.Ausgabe von Aufgabe_TQ_05_3.java
a) Testtabelle (W = true, F = false, Exception = IllegalArgumentException):
TF | Testdaten | erwartet | fehlerhaft | korrigiert
1 | {W, W, F, W, W, W, F} | 3 | 3 | 3
2 | {W, F, W, W, W, W} | 4 | 1 | 4 <- Abweichung
3 | {F, F, F} | 0 | 0 | 0
4 | {W, W, W} | 3 | 0 | 3 <- Abweichung
5 | {} (leeres Array) | Exception | 0 | Exception <- Abweichung
6 | null | Exception | Exception | Exception
b) Schreibtischtest serieAb(t, 3) mit t = {F, W, F, W, W, W}:
Aufruf 1: serieAb(t, 3) i >= t.length: false, t[i]: true -> 1 + serieAb(t, 4)
Aufruf 2: serieAb(t, 4) i >= t.length: false, t[i]: true -> 1 + serieAb(t, 5)
Aufruf 3: serieAb(t, 5) i >= t.length: false, t[i]: true -> 1 + serieAb(t, 6)
Aufruf 4: serieAb(t, 6) i >= t.length: true, t[i]: nicht geprüft -> Basisfall, Rückgabe 0
Aufruf 3 liefert 1
Aufruf 2 liefert 2
Aufruf 1 liefert 3
Ergebnis: 3
d) Ausgeführte Zeilen der fehlerhaften Methode je Testfall:
TF1: [2, 5, 6, 7, 8, 9, 11, 12, 14, 17]
TF2: [2, 5, 6, 7, 8, 9, 11, 12, 14, 17]
TF3: [2, 5, 6, 7, 8, 11, 14, 17]
TF4: [2, 5, 6, 7, 8, 9, 17]
TF5: [2, 5, 6, 7, 17]
TF6: [2, 3]
TF1 + TF6 zusammen: [2, 3, 5, 6, 7, 8, 9, 11, 12, 14, 17] -> alle Anweisungszeilen? true
Erklärung: Aufbau wie W25 T2 A3b (Tabelle „erwartetes Testergebnis / Testergebnis“, danach zwei Fehler korrigieren). Der Fehler „letztes Element/letzte Serie wird nicht ausgewertet“ ist derselbe Mechanismus wie die fehlende letzte Gruppe beim Gruppenwechsel. d) zeigt: Volle Anweisungsüberdeckung mit TF1 + TF6 findet keinen der beiden Fehler!
| ÄK | gültig/ungültig | Beschreibung | Repräsentant |
|---|---|---|---|
| 1 | gültig | ganze Zahl von 0 bis 3 | 2 |
| 2 | ungültig | ganze Zahl kleiner als 0 | −2 |
| 3 | ungültig | ganze Zahl größer als 3 | 5 |
| 4 | ungültig | keine ganze Zahl (Text, Kommazahl, leeres Feld) | „zwei“ bzw. 1,5 bzw. leer |
Testwerte: −1, 0, 3, 4 (je der Wert auf der Grenze und der direkt benachbarte Wert außerhalb). Grenzwerte werden zusätzlich getestet, weil Programmierfehler besonders häufig an den Rändern der Klassen entstehen (< statt <=, Off-by-one); ein Repräsentant aus der Mitte der Klasse würde solche Fehler nicht finden.
| Parameter | gültige Äquivalenzklassen | ungültige Äquivalenzklassen |
|---|---|---|
| alter | A1: 14–17 (50 % Ermäßigung) A2: 18–64 (voller Beitrag) A3: 65–120 (20 % Ermäßigung) | A4: kleiner als 14 A5: größer als 120 |
| tarif | T1: „BASIC“ T2: „PREMIUM“ | T3: anderer Text (z. B. „GOLD“, „basic“, leere Zeichenkette) T4: null |
| Nr. | alter | tarif | abgedeckte ÄK | erwartetes Ergebnis |
|---|---|---|---|---|
| 1 | 15 | „BASIC“ | A1, T1 | 14,95 (29,90 · 0,5) |
| 2 | 40 | „PREMIUM“ | A2, T2 | 49,90 |
| 3 | 70 | „BASIC“ | A3, T1 | 23,92 (29,90 · 0,8) |
| 4 | 10 | „BASIC“ | A4 (ungültig) | IllegalArgumentException |
| 5 | 130 | „PREMIUM“ | A5 (ungültig) | IllegalArgumentException |
| 6 | 40 | „GOLD“ | T3 (ungültig) | IllegalArgumentException |
| 7 | 40 | null | T4 (ungültig) | IllegalArgumentException |
7 Testfälle sind das Minimum: Die drei gültigen Altersklassen brauchen drei Testfälle (die beiden gültigen Tarife werden dabei mit abgedeckt), jede der vier ungültigen Klassen wird einzeln mit sonst gültigen Werten getestet.
| alter | erwartetes Ergebnis | alter | erwartetes Ergebnis |
|---|---|---|---|
| 13 | IllegalArgumentException | 64 | 29,90 |
| 14 | 14,95 | 65 | 23,92 |
| 17 | 14,95 | 120 | 23,92 |
| 18 | 29,90 | 121 | IllegalArgumentException |
Ausgabe von Aufgabe_TQ_06_2.java (Referenzimplementierung nach Anlage 3)
b) Testfälle aus Äquivalenzklassen (gültige kombiniert, ungültige einzeln): T1 beitrag( 15, "BASIC" ) erwartet 14.95 tatsächlich 14.95 OK T2 beitrag( 40, "PREMIUM") erwartet 49.90 tatsächlich 49.90 OK T3 beitrag( 70, "BASIC" ) erwartet 23.92 tatsächlich 23.92 OK T4 beitrag( 10, "BASIC" ) erwartet Exception tatsächlich Exception OK T5 beitrag(130, "PREMIUM") erwartet Exception tatsächlich Exception OK T6 beitrag( 40, "GOLD" ) erwartet Exception tatsächlich Exception OK T7 beitrag( 40, null ) erwartet Exception tatsächlich Exception OK c) Grenzwerte für das Alter (Tarif BASIC): G1 beitrag( 13, "BASIC" ) erwartet Exception tatsächlich Exception OK G2 beitrag( 14, "BASIC" ) erwartet 14.95 tatsächlich 14.95 OK ... G8 beitrag(121, "BASIC" ) erwartet Exception tatsächlich Exception OK -> 15 bestanden, 0 fehlgeschlagen
null und „anderer Text“ dürfen zu einer Klasse zusammengefasst werden. b) 2 P gültige Klassen mit möglichst wenigen kombinierten Testfällen, 2 P ungültige Klassen einzeln, 1 P richtige erwartete Ergebnisse. c) 8 Werte mit Ergebnis 3 P (je fehlendem/falschem Wert 0,5 P Abzug).Erklärung: Die Testfälle stammen ausschließlich aus der Spezifikation (Black-Box) – der Code ist hier noch gar nicht bekannt. Grenzwert 65 ist der wichtigste: An ihm scheitert die Implementierung in TQ-06.3.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class BeitragsrechnerTest {
private Beitragsrechner rechner;
@BeforeEach
void setUp() {
rechner = new Beitragsrechner();
}
@Test
void tf1JugendlicherPremiumHalberBeitrag() {
assertEquals(24.95, rechner.beitrag(16, "PREMIUM"), 0.001);
}
@Test
void tf2ErwachsenerBasicVollerBeitrag() {
assertEquals(29.90, rechner.beitrag(40, "BASIC"), 0.001);
}
@Test
void tf3SeniorAb65Ermaessigt() {
assertEquals(23.92, rechner.beitrag(65, "BASIC"), 0.001);
}
@Test
void tf4AlterUnter14WirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> rechner.beitrag(13, "BASIC"));
}
@Test
void tf5UnbekannterTarifWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> rechner.beitrag(30, "GOLD"));
}
@Test
void tf6FehlenderTarifWirdAbgelehnt() {
assertThrows(IllegalArgumentException.class, () -> rechner.beitrag(30, null));
}
}
alter > 65; für 65 ist die Bedingung falsch, die Methode liefert in Zeile 19 den vollen Beitrag 29,90 statt 23,92 – laut Beitragsordnung gilt die Ermäßigung ab 65.tarif.equals("BASIC") auf null auf → NullPointerException statt der geforderten IllegalArgumentException. assertThrows erwartet genau diesen Typ (oder eine Unterklasse); eine NPE ist keine IllegalArgumentException, der Test schlägt daher fehl.06 if ("BASIC".equals(tarif)) { // null-sicher: liefert false statt NPE
08 } else if ("PREMIUM".equals(tarif)) {
16 if (alter >= 65) {
Gleichwertig für Zeile 06: vorher if (tarif == null) { throw new IllegalArgumentException("Tarif fehlt"); } einfügen.
5 Testfälle: TF1, TF2, TF3, TF4 und TF5 (statt TF5 auch TF6). Begründung: Jeder Aufruf endet an genau einem von fünf Ausgängen – throw in Zeile 03 (nur TF4), throw in Zeile 11 (TF5 oder TF6), return in Zeile 14 (nur TF1), Zeile 17 (nur TF3) oder Zeile 19 (nur TF2). Diese Anweisungen schließen sich gegenseitig aus, also sind mindestens fünf Testfälle nötig. Die übrigen Anweisungen (Zeile 07 BASIC, Zeile 09 PREMIUM) werden dabei mit ausgeführt (TF2/TF3 bzw. TF1).
Pro: Automatisiert kostet die Ausführung kaum Zeit; alle Altersgrenzen sind sicher enthalten, Denkfehler bei der Klassenbildung fallen nicht ins Gewicht. Contra: Für jeden der 262 Fälle muss das erwartete Ergebnis unabhängig aus der Spezifikation bestimmt werden – wer es mit dem Code berechnet, testet den Code gegen sich selbst. Die meisten Fälle sind redundant (gleiche Äquivalenzklasse), die Tests werden unübersichtlich und schwer wartbar. Trotzdem fehlen wichtige Fälle: unbekannter Tarif, null, extreme Werte außerhalb 0–130. Fazit: Äquivalenzklassen plus Grenzwerte erreichen mit rund 15 Testfällen praktisch dieselbe Fehlerfindung; vollständiges Testen aller Eingaben ist im Allgemeinen unmöglich. Der Vorschlag ist höchstens als ergänzender parametrisierter Test sinnvoll, ersetzt aber die gezielten Testfälle nicht.
Ausgabe von Aufgabe_TQ_06_3.java (Testmethoden mit Ersatz für assertEquals/assertThrows)
b) Tests gegen die FEHLERHAFTE Methode (Anlage 4): OK TF1 tf1JugendlichPremium OK TF2 tf2ErwachsenBasic FEHLER TF3 tf3SeniorAbGrenze65 -> expected: <23.92> but was: <29.9> OK TF4 tf4AlterZuNiedrig OK TF5 tf5TarifUnbekannt FEHLER TF6 tf6TarifFehlt -> Unexpected exception type thrown, expected: <IllegalArgumentException> but was: <NullPointerException> 4 von 6 Tests bestanden c) Tests gegen die KORRIGIERTE Methode: ... 6 von 6 Tests bestanden d) Ausgeführte Anweisungszeilen der korrigierten Methode je Testfall: TF1: [2, 6, 8, 9, 13, 14] TF2: [2, 6, 7, 13, 16, 19] TF3: [2, 6, 7, 13, 16, 17] TF4: [2, 3] TF5: [2, 6, 8, 11] TF6: [2, 6, 8, 11] minimale Anzahl für 100 % Anweisungsüberdeckung: 5 -> [TF1 TF2 TF3 TF4 TF5, TF1 TF2 TF3 TF4 TF6]
assertThrows mit Lambda); @BeforeEach ist nicht gefordert, new Beitragsrechner() in jeder Methode ist gleichwertig. b) je fehlschlagendem Testfall 2 P (Testfall 1 P, Begründung mit Zeile 1 P). c) je Korrektur 1 P. d) 1 P Anzahl, 2 P Begründung. e) 1 P Pro, 1 P Contra, 1 P begründetes Fazit.Erklärung: TF6 ist eine typische Falle: Auch eine „falsche“ Exception lässt den Test fehlschlagen. In der Prüfung genügt die Aussage, dass eine NullPointerException statt der erwarteten Exception ausgelöst wird.
Drei der folgenden Maßnahmen, jeweils mit Erläuterung:
| Zeile | Verstoß |
|---|---|
| 02, 03 | Nichtssagende Namen p, n, e, s, x – der Zweck ist ohne Erklärung nicht erkennbar. |
| 05, 07, 10, 12, 15 (06, 11) | „Magic Numbers“ 10.0, 15.0, 0.1, 5.0 und 10 statt benannter Konstanten – Preise stehen mehrfach im Code, eine Preisänderung muss an mehreren Stellen erfolgen. |
| 04–14 | Doppelter Code (Verstoß gegen DRY): Die Rabattberechnung ist für ermäßigt und normal kopiert. |
| 01, 15 | Nutzlose Kommentare: „Preis“ und „plus 5 mal n“ wiederholen nur den Code, statt den Zweck zu erklären; ein Javadoc-Kommentar fehlt. |
| 04, 15 | == true ist überflüssig (if (e) genügt). |
| 15 | if ohne geschweifte Klammern und mit der Anweisung in derselben Zeile – verstößt gegen übliche Konventionen und ist fehleranfällig bei späteren Ergänzungen. |
private static final double PREIS_TAGESKARTE = 15.00;
private static final double PREIS_TAGESKARTE_ERMAESSIGT = 10.00;
private static final double SAUNA_ZUSCHLAG_JE_KARTE = 5.00;
private static final int MENGENRABATT_AB_ANZAHL = 10;
private static final double MENGENRABATT_SATZ = 0.10;
/**
* Berechnet den Gesamtpreis für Gast-Tageskarten nach der Preisliste (Anlage 3).
* Ab 10 Karten wird der Kartenpreis um 10 % ermäßigt;
* der Sauna-Zuschlag wird nicht rabattiert.
*
* @param anzahlKarten Anzahl der gekauften Tageskarten (mindestens 1)
* @param ermaessigt true, wenn ein Ermäßigungsnachweis (Schüler, Studierende) vorliegt
* @param mitSauna true, wenn die Karten die Saunanutzung einschließen
* @return Gesamtpreis in Euro
*/
public double berechneGastkartenPreis(int anzahlKarten, boolean ermaessigt, boolean mitSauna) {
double einzelpreis = ermaessigt ? PREIS_TAGESKARTE_ERMAESSIGT : PREIS_TAGESKARTE;
double preis = einzelpreis * anzahlKarten;
if (anzahlKarten >= MENGENRABATT_AB_ANZAHL) {
preis = preis * (1 - MENGENRABATT_SATZ);
}
if (mitSauna) {
preis = preis + SAUNA_ZUSCHLAG_JE_KARTE * anzahlKarten;
}
return preis;
}
Optional (über die Aufgabe hinaus, weil das Verhalten sich ändert): ungültige Anzahl (≤ 0) mit IllegalArgumentException abweisen; Geldbeträge in der Praxis als BigDecimal oder in Cent (long) rechnen, um Rundungsfehler zu vermeiden.
Ausgabe von Aufgabe_TQ_07_2.java – Regressionsvergleich alte/neue Methode
Regressionsvergleich alt/neu: 100 Kombinationen, 0 Abweichungen Beispiele: 1 Karte normal = 15,00 | 3 Karten ermäßigt + Sauna = 45,00 | 10 Karten normal = 135,00 | 10 Karten normal + Sauna = 185,00
?:, z. B. mit if/else für den Einzelpreis). c) 1 P Beschreibung, 1 P @param und @return.Erklärung: Ein Refactoring darf das Verhalten nicht ändern – deshalb vergleicht die Java-Datei alte und neue Methode für 100 Eingabekombinationen (ein kleiner automatischer Regressionstest).
Fehler (Logik, Design, Sicherheitslücken) werden früh und günstig gefunden · Vier-Augen-Prinzip, Prüfung gegen die Anforderungen · Wissenstransfer, mehrere Entwickler kennen jeden Codeteil · einheitlicher Stil, bessere Lesbarkeit und Wartbarkeit · Lerneffekt für neue Kollegen und Auszubildende.
Findet automatisch (zwei genügen): Verstöße gegen Namens- und Formatierungskonventionen · ungenutzte Variablen, toter Code · mögliche NullPointerExceptions · duplizierter Code · zu hohe Komplexität, zu lange Methoden · bekannte unsichere Muster (z. B. SQL per String-Verkettung).
Findet nicht: fachliche Fehler gegenüber der Spezifikation, z. B. alter > 65 statt alter >= 65 – der Code ist formal einwandfrei (ebenso: fehlende Funktionen, ob Namen und Design wirklich verständlich sind).
Für den Vorschlag spricht: Statische Analyse und Unit-Tests laufen automatisch bei jedem Commit, schnell, lückenlos und objektiv; 85 % Testabdeckung ist ein guter Wert; die eingesparte Zeit hilft vor der Abnahme.
Dagegen spricht:
Fazit: Den Vorschlag in dieser Form ablehnen. Automatische Prüfungen sind eine gute Grundlage, ersetzen Reviews aber nicht. Sinnvoller Kompromiss: Reviews beibehalten, mindestens für sicherheitskritische und fachlich komplexe Teile (Zugangsprüfung, Beitragsberechnung, GesundPlus-Schnittstelle), kleine Änderungen mit leichtgewichtigem Review.
Zwei Entwickler arbeiten gemeinsam an einem Rechner an derselben Aufgabe: Der Driver schreibt den Code, der Navigator prüft jede Zeile laufend, denkt an Sonderfälle und die Gesamtstruktur; die Rollen wechseln regelmäßig. Vorteil: Der Code wird sofort beim Entstehen geprüft („Review in Echtzeit“), Fehler in der sicherheitsrelevanten Zugangsprüfung werden früher gefunden, und zwei Personen kennen den Code. Nachteil: Zwei Personen arbeiten an einer Aufgabe – höherer Personalaufwand, gerade unter Zeitdruck; zudem anstrengend und abhängig davon, ob das Paar gut zusammenarbeitet.
Erklärung: Entspricht W21 T1 A4cb. Bei „Beurteilen“ zählt die Abwägung: Argumente beider Seiten, bezogen auf die Situation, dann ein klares Fazit.