Block 05 · Testen & Qualitätssicherung – Lösungen

Lösungssatz zu 05_Testen_QS.html · JUnit-5-Code im Text, dazu lauffähige Java-Dateien mit eigener kleiner Testhilfe (ohne JUnit) im Ordner java\05_Testen_QS\ (Start mit java Aufgabe_TQ_NN_k.java)
Hinweise zur Bewertung Wie in den IHK-Lösungshinweisen gilt: Andere sach- und fachgerechte Lösungen werden mit voller Punktzahl bewertet. Bei Wissensfragen sind die Listen beispielhaft – es zählen fachlich richtige, auf die Situation bezogene Antworten. Bei Code-Korrekturen kosten Syntaxfehler in der Regel keine Punkte, Logikfehler schon. Beachte die Operatoren: „nennen“ = Stichwort genügt, „beschreiben/erläutern“ = ganze Sätze mit Zweck bzw. Begründung. Alle Java-Ausgaben unten stammen aus einem echten Programmlauf (JDK 21).

Lösungen

TQ-01 · Testarten

Aufgabe TQ-01.1 · 6 Punkte

a) Teststufen (4 P)

Nr.TeststufePrüfgrundlage (V-Modell)
1Komponententest (Unit-Test, Modultest)Komponentenspezifikation (Modul-/Methodenentwurf; hier die Spezifikation der Methode beitrag in Anlage 3)
2Integrationstesttechnischer Systementwurf (Architektur, Schnittstellenbeschreibung der REST-API)
3Systemtestfunktionaler Systementwurf (Pflichtenheft)
4AbnahmetestAnforderungsdefinition (Lastenheft, Vertrag)

b) Statisch vs. dynamisch (2 P)

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.

Bewertung: a) je Zeile 1 P (0,5 P Teststufe, 0,5 P Prüfgrundlage); „Modultest“ und „Unit-Test“ gleichwertig; bei Nr. 3 auch „Anforderungen aus dem Pflichtenheft“. b) 1 P Unterschied, 1 P passende Beispiele.

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.

Aufgabe TQ-01.2 · 10 Punkte

a) Black-Box vs. White-Box (4 P)

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):

b) Regressionstest (3 P)

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.

c) Lasttest (3 P)

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.

Bewertung: a) 2 P Unterschied (je Testart 1 P), je Vorteil 1 P. b) 1 P Testart, 1 P Erläuterung, 1 P Begründung der Automatisierung. c) 1 P Testart (Last-, Performance- oder Stresstest), 2 P Vorgehen (Simulation der Last + Messgrößen/Vergleich mit Anforderung).

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.

Aufgabe TQ-01.3 · 20 Punkte

a) Inhalte des Testkonzepts (6 P) – drei der folgenden

b) Usability-Test (4 P) – zwei der folgenden

c) Smoke-Test (3 P)

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.

da) Konformität zur Spezifikation (3 P) – zwei der folgenden

db) Stellungnahme (4 P)

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.

Bewertung: a) je beschriebenem Inhalt 2 P (1 P Nennung, 1 P Beschreibung mit Bezug zum Projekt). b) je Methode 2 P. c) 1 P Begriff, je Prüfung 1 P. da) je Maßnahme 1,5 P. db) 1 P klare Position, 3 P für mindestens drei Argumente (Struktur vs. Inhalt, Fachregeln, Übertragung/Datenschutz/Gegenstelle).

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.

TQ-02 · Code analysieren / Fehler finden

Aufgabe TQ-02.1 · 5 Punkte

a) Fehlerarten (3 P)

Nr.FehlerartBegründung
1SyntaxfehlerCompiler meldet den Fehler, Programm wird nicht übersetzt
2LaufzeitfehlerIndex length existiert nicht → ArrayIndexOutOfBoundsException, Abbruch
3Logikfehlerläuft, liefert aber eine falsche (negative) Summe
4Logikfehlerläuft, liefert aber 20 % statt 80 % des Preises
5Syntaxfehlerunbekannter Bezeichner, Compiler meldet den Fehler
6LaufzeitfehlerMethodenaufruf auf null → NullPointerException

b) Logikfehler (2 P)

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.

Bewertung: a) je richtig gesetztem Kreuz 0,5 P. b) 1 P Begründung, 1 P Vorgehen (Test mit Soll-Ergebnis oder Review).
Aufgabe TQ-02.2 · 10 Punkte

a) Schreibtischtest (3 P)

ipuls[i]Bedingung Zeile 05 (> 140)summeanzahl
1120falsch00
2140falsch00
3160wahr1601
4135falsch1601

Tatsächliches Ergebnis: 160 / 5 = 32.0. Erwartetes Ergebnis: Werte ≥ 140 sind 150, 140 und 160 → (150 + 140 + 160) / 3 = 150.0.

b) Fehler (3 P)

ZeileFehler und Auswirkung
04Die 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).
13Division durch puls.length statt durch anzahl → es wird durch die Anzahl aller Werte geteilt, der Durchschnitt ist viel zu klein.

c) Korrigierte Zeilen (2 P)

04     for (int i = 0; i < puls.length; i++) {
05         if (puls[i] >= grenze) {
13     return (double) summe / anzahl;

d) Testdaten mit „zufällig richtigem“ Ergebnis (2 P)

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
Bewertung: a) 2 P Tabelle (Folgefehler werden nicht doppelt abgezogen), 1 P beide Ergebnisse. b) je Fehler mit Zeile und Auswirkung 1 P. c) drei richtige Zeilen 2 P, zwei richtige 1 P; Java-Alternative 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.

Aufgabe TQ-02.3 · 25 Punkte

a) Datenverlaufstabelle und tatsächliche Ausgabe (6 P)

TypVariableWerteverlauf
integeri0, 1, 2, 3, 4, 5, 6
stringkursAqua-Fitness, Rückenfit, Yoga
integeranzahl0, 1, 2, 0, 1, 2, 3, 0, 1, 2
integersumme0, 14, 32, 0, 10, 22, 30, 0, 16, 29
integermax0, 14, 18 (bleibt bis zum Ende 18)
integeranzahlGesamt0, 2, 5
integersummeGesamt0, 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

b) Fehler (6 P)

ZeileFehler (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).
27Division durch anzahl (Termine des letzten Kurses) statt durch anzahlGesamt.Gesamtdurchschnitt 62 / 2 = 31.0 statt 13.0.

c) Korrekturen (8 P)

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

d) Leere Liste (3 P)

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

e) Testarten nach der Korrektur (2 P)

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
Bewertung: a) 4 P Datenverlaufstabelle (je zwei richtige Zeilen ca. 1 P; Zwischenwerte 0 nach dem Zurücksetzen dürfen fehlen), 2 P tatsächliche Ausgabe. b) je Fehler 2 P (1 P Ursache mit Zeile, 1 P Auswirkung). c) 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.

TQ-03 · Unit-Test / Überdeckungsmaße

Aufgabe TQ-03.1 · 6 Punkte

a) Unit-Test (2 P)

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).

b) Eigenschaften (2 P) – zwei der folgenden

c) Arrange – Act – Assert (2 P)

ZeilePhaseAufgabe
(1)ArrangeAusgangszustand herstellen: Testobjekt anlegen (Kurs „Yoga“ mit 12 Plätzen, 0 gebucht).
(2)ActDie zu testende Aktion ausführen: 3 Plätze buchen.
(3)AssertTatsä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
Bewertung: a) 1 P Begriff (kleinste Einheit, isoliert, automatisiert, Soll-Ist-Vergleich), 1 P Zweck. b) je Eigenschaft mit Beschreibung 1 P. c) 1 P richtige Zuordnung, 1 P Beschreibung der Phasen.
Aufgabe TQ-03.2 · 12 Punkte

a) Auswertung der Testfälle (6 P)

TestfallAufrufRückgabewerterwartetErgebnis
1freiePlaetze(20, 15)55OK
2freiePlaetze(20, 0)−1 (gebucht > 0 falsch)20Fehler
3freiePlaetze(20, 20)−1 (20 < 20 falsch)0Fehler
4freiePlaetze(20, 21)−1−1OK
5freiePlaetze(0, 0)−1−1OK
6freiePlaetze(20, 1)1919OK

b) Korrektur (3 P)

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 }

c) JUnit bricht beim ersten Fehlschlag ab (3 P)

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
Bewertung: a) je Testfall 1 P (Rückgabewert und OK/Fehler). b) je korrigierter Bedingung 1,5 P; gleichwertig ist eine zusammengefasste Bedingung 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.

Aufgabe TQ-03.3 · 25 Punkte

a) Überdeckungsmaße (9 P)

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.

b) Kontrollflussgraph (5 P)

Start (02) Ende (18) B1 A1 B2 B3 A2 A3 B4 A4 A5 w f w f w f w f Legende Start = Zeile 02 (Initialisierung) Ende = Zeile 18 (return) B1 … B4 = Bedingungen A1 … A5 = Anweisungen w / f = wahr / falsch
Abb. L-1: Kontrollflussgraph der Methode zugangPruefen (B-Knoten hinterlegt)

c) Mindestanzahl der Testfälle (4 P)

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 aktivPremiumoffener BetragBereichdurchlaufene ZweigeRückgabe
T1neinbeliebigbeliebigbeliebigB1 f„Kein Zutritt“
T2jaja25,50 €SaunaB1 w, B2 w, B3 w, B4 w„Zutritt Sauna (Zahlung offen)“
T3janein0,00 €SaunaB1 w, B2 w, B3 f, B4 f„Sauna nur mit Premium“
T4jabeliebigbeliebigTrainingsbereichB1 w, B2 f, B4 w oder f„Zutritt Trainingsbereich …“

d) Alle Pfade (5 P)

NummerPfadBeispiel
1B1, A5Vertrag nicht aktiv
2B1, A1, B2, B3, A2, B4, A4Sauna, Premium, Zahlung offen
3B1, A1, B2, B3, A2, B4Sauna, Premium, nichts offen
4B1, A1, B2, B3, A3, B4, A4Sauna, kein Premium, Zahlung offen
5B1, A1, B2, B3, A3, B4Sauna, kein Premium, nichts offen
6B1, A1, B2, B4, A4Trainingsbereich, Zahlung offen
7B1, A1, B2, B4Trainingsbereich, 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.

e) Überdeckung beweist keine Korrektheit (2 P)

Ü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
Bewertung: a) je Maß 3 P (2 P Erläuterung, 1 P Verhältnis zu den anderen Maßen). b) 5 P: alle Knoten (2 P), richtige Kanten inkl. der falsch-Kante B2 → B4 und B1 → A5 (2 P), Beschriftung w/f (1 P); Knoten mit Zeilennummern statt Marken sind gleichwertig. c) je 2 P (Anzahl + Begründung bzw. Testfälle). d) 5 P für alle sieben Pfade, je fehlendem oder falschem Pfad 1 P Abzug. e) 1 P Begründung, 1 P Beispiel.

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).

TQ-04 · Qualitätsmerkmale ISO/IEC 25010

Aufgabe TQ-04.1 · 6 Punkte
Nr.QualitätsmerkmalUntermerkmal (nicht gefordert)
1LeistungseffizienzZeitverhalten
2ZuverlässigkeitFehlertoleranz (auch Verfügbarkeit)
3BenutzbarkeitErlernbarkeit
4KompatibilitätInteroperabilität
5WartbarkeitModularität, Modifizierbarkeit
6SicherheitVertraulichkeit
Bewertung: je richtiger Zuordnung 1 P. Begriffe der ISO 9126 werden ebenfalls akzeptiert (Effizienz, Änderbarkeit; Nr. 4 dort Funktionalität/Interoperabilität).

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).

Aufgabe TQ-04.2 · 8 Punkte

a) Zuverlässigkeit (3 P)

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):

b) Wartbarkeit (3 P)

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.

c) Messbare Anforderungen (2 P)

Bewertung: a) 1 P Beschreibung, je Untermerkmal mit Bezug 1 P. b) 1 P Beschreibung, 1 P zwei Untermerkmale, 1 P Begründung. c) je messbarer Anforderung (Messgröße + Zielwert + Bedingung) 1 P.
Aufgabe TQ-04.3 · 17 Punkte

a) ISO/IEC 9126 (3 P)

Funktionalität · Zuverlässigkeit · Benutzbarkeit · Effizienz · Änderbarkeit (Wartbarkeit) · Übertragbarkeit

b) Neu in ISO/IEC 25010 (2 P)

Sicherheit (Informationssicherheit; in 9126 Untermerkmal der Funktionalität) und Kompatibilität (fasst Interoperabilität und Koexistenz zusammen).

c) Qualitätsanforderungen „Check-in am Drehkreuz“ (9 P) – Beispiellösung

Merkmalkonkrete Anforderungmessbares AbnahmekriteriumPrüfung
ZuverlässigkeitDer 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)
Leistungs­effizienzDas 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
SicherheitNur 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

d) Zielkonflikt (3 P) – ein Beispiel genügt

Bewertung: a) je Merkmal 0,5 P. b) je Merkmal 1 P. c) je Merkmal 3 P (je Spalte 1 P); Kriterien müssen wirklich messbar sein (Zahl + Einheit/Bedingung) – „schnell“ oder „sicher“ allein gibt 0 P in dieser Spalte. d) 1 P die beiden Merkmale, 2 P Erläuterung am Beispiel mit Abwägung.

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.

TQ-05 · Schreibtischtest

Aufgabe TQ-05.1 · 5 Punkte

a) Schreibtischtest (1 P)

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.

b) Trace-Tabelle (3 P)

iminuten[i]Bedingung Zeile 04Bedingung Zeile 07punkte
––––0
045falschwahr1
170wahr–4
220falschfalsch4
360wahr–7
430falschwahr8

c) Rückgabewert (1 P)

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
Bewertung: a) 1 P. b) je richtiger Zeile 0,5 P (max. 3 P, Folgefehler nicht doppelt abziehen). c) 1 P.

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.

Aufgabe TQ-05.2 · 10 Punkte

a) Schreibtischtest (6 P)

tagstdbesucher[tag][std]Bedingung Zeile 06 (> 80)folgeanzahl
––––00
0040falsch00
0185wahr10
0290wahr21
0395wahr31
1070falsch01
1182wahr11
1260falsch01
1388wahr11
2091wahr22
2150falsch02
2296wahr12
2397wahr23

b) Ergebnisse (2 P)

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.

c) Fehler und Korrektur (2 P)

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)
Bewertung: a) je zwei richtige Zeilen 1 P (Folgefehler nicht doppelt abziehen). b) 1 P beide Ergebnisse, 1 P Begründung. c) 1 P Fehler mit Zeile, 1 P Korrektur (gleichwertig: 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.

Aufgabe TQ-05.3 · 20 Punkte

a) Testtabelle (6 P)

TFTestdatenerwartetes Ergebnistatsächliches Ergebnis
1{W, W, F, W, W, W, F}33
2{W, F, W, W, W, W}41
3{F, F, F}00
4{W, W, W}30
5{}IllegalArgumentException0
6nullIllegalArgumentExceptionIllegalArgumentException

b) Schreibtischtest der Rekursion (5 P)

Aufrufii >= t.lengtht[i]Rückgabewert
13falschW (true)1 + Ergebnis Aufruf 2 = 3
24falschW (true)1 + Ergebnis Aufruf 3 = 2
35falschW (true)1 + Ergebnis Aufruf 4 = 1
46wahrnicht ausgewertet0 (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.

c) Fehler und Korrektur (5 P)

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;
}

d) Mindestauswahl für Anweisungsüberdeckung der gegebenen Methode (4 P)

Es genügen 2 Testfälle: TF6 und TF1 (statt TF1 auch TF2).

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
Bewertung: a) je Zeile 1 P (0,5 P erwartet, 0,5 P tatsächlich). b) je Aufruf 1 P, 1 P Ergebnis. c) je Fehler 1 P Benennung mit Zeile, 1,5 P Korrektur. d) 1 P Anzahl, 1 P TF6, 2 P Begründung des zweiten Testfalls.

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!

TQ-06 · Äquivalenzklassen / Testfälle

Aufgabe TQ-06.1 · 5 Punkte

a) Äquivalenzklassen (3 P)

ÄKgültig/ungültigBeschreibungRepräsentant
1gültigganze Zahl von 0 bis 32
2ungültigganze Zahl kleiner als 0−2
3ungültigganze Zahl größer als 35
4ungültigkeine ganze Zahl (Text, Kommazahl, leeres Feld)„zwei“ bzw. 1,5 bzw. leer

b) Grenzwerte (2 P)

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.

Bewertung: a) gültige Klasse 1 P, die beiden Bereichsüberschreitungen 1 P, formale Klasse (keine Zahl/leer) 1 P – jeweils mit Repräsentant. Wer „leer“ als eigene fünfte Klasse führt, ist ebenfalls richtig. b) 1 P Werte, 1 P Begründung.
Aufgabe TQ-06.2 · 12 Punkte

a) Äquivalenzklassen (4 P)

Parametergültige Äquivalenzklassenungültige Äquivalenzklassen
alterA1: 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
tarifT1: „BASIC“
T2: „PREMIUM“
T3: anderer Text (z. B. „GOLD“, „basic“, leere Zeichenkette)
T4: null

b) Testfalltabelle (5 P)

Nr.altertarifabgedeckte ÄKerwartetes Ergebnis
115„BASIC“A1, T114,95 (29,90 · 0,5)
240„PREMIUM“A2, T249,90
370„BASIC“A3, T123,92 (29,90 · 0,8)
410„BASIC“A4 (ungültig)IllegalArgumentException
5130„PREMIUM“A5 (ungültig)IllegalArgumentException
640„GOLD“T3 (ungültig)IllegalArgumentException
740nullT4 (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.

c) Grenzwerte für alter, Tarif BASIC (3 P)

altererwartetes Ergebnisaltererwartetes Ergebnis
13IllegalArgumentException6429,90
1414,956523,92
1714,9512023,92
1829,90121IllegalArgumentException

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
Bewertung: a) je Parameter 2 P (gültige 1 P, ungültige 1 P); 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.

Aufgabe TQ-06.3 · 18 Punkte

a) JUnit-5-Testklasse (6 P)

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));
    }
}

b) Fehlschlagende Testfälle (4 P)

c) Korrektur (2 P)

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.

d) Kleinste Teilmenge für Anweisungsüberdeckung (3 P)

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).

e) Beurteilung „alle Alterswerte 0–130 testen“ (3 P)

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]
Bewertung: a) je Testmethode 1 P (Aufruf, erwarteter Wert, Delta bzw. 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.

TQ-07 · Codequalität / Reviews

Aufgabe TQ-07.1 · 6 Punkte

Drei der folgenden Maßnahmen, jeweils mit Erläuterung:

Bewertung: je Maßnahme 2 P (1 P Nennung, 1 P Erläuterung, wie sie die Qualität verbessert). Drei Testarten (z. B. Unit-, Integrations-, Systemtest) zählen zusammen höchstens als zwei Maßnahmen.
Aufgabe TQ-07.2 · 10 Punkte

a) Verstöße (4 P) – vier der folgenden

ZeileVerstoß
02, 03Nichtssagende 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–14Doppelter Code (Verstoß gegen DRY): Die Rabattberechnung ist für ermäßigt und normal kopiert.
01, 15Nutzlose 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).
15if ohne geschweifte Klammern und mit der Anweisung in derselben Zeile – verstößt gegen übliche Konventionen und ist fehleranfällig bei späteren Ergänzungen.

b) Überarbeitete Methode (4 P) und c) Javadoc (2 P)

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
Bewertung: a) je Verstoß mit Zeile 1 P. b) 1 P sprechende Namen, 1 P Konstanten, 1 P Duplikat beseitigt, 1 P Verhalten unverändert (auch ohne Bedingungsoperator ?:, 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).

Aufgabe TQ-07.3 · 18 Punkte

a) Ablauf eines Code-Reviews per Pull-Request (4 P)

  1. Pull-Request erstellen: Der Entwickler pusht seine Änderungen in einen eigenen Branch und eröffnet einen Pull-Request mit Beschreibung (was, warum, wie getestet); die CI-Pipeline baut und testet automatisch.
  2. Prüfung durch Reviewer: Mindestens ein anderer Entwickler liest die Änderungen, prüft sie anhand einer Checkliste (Logik, Spezifikation, Lesbarkeit, Tests, Sicherheit) und schreibt Kommentare direkt an die betroffenen Zeilen.
  3. Nacharbeit: Der Autor beantwortet die Kommentare, behebt die Befunde und lädt die Änderungen hoch.
  4. Abschluss: Der Reviewer kontrolliert die Nacharbeit und gibt frei (Approve); danach wird der Branch in den Hauptzweig übernommen (Merge).

b) Vorteile (3 P) – drei der folgenden

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.

c) Statische Codeanalyse (3 P)

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).

d) Beurteilung des Vorschlags (5 P)

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.

e) Pair Programming (3 P)

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.

Bewertung: a) je Schritt 1 P. b) je Vorteil 1 P. c) 2 P für zwei automatisch findbare Probleme, 1 P für ein nicht findbares. d) 1 P Pro-Argument(e), 3 P Contra-Argumente (mind. drei verschiedene), 1 P begründetes Fazit; ein gut begründetes „Ja, aber …“ ist ebenfalls möglich. e) 1 P Verfahren, je 1 P Vor- und Nachteil.

Erklärung: Entspricht W21 T1 A4cb. Bei „Beurteilen“ zählt die Abwägung: Argumente beider Seiten, bezogen auf die Situation, dann ein klares Fazit.