Übungsaufgaben zu Unit-Tests, Testfällen und Fehlersuche im Code

https://bildung.social/@oerinformatik/

https://oer-informatik.de/unit-tests-uebungsaufgaben

Unit-Tests, JUnit, Parametrisierte Tests, Testfälle, Äquivalenzklassen, Schreibtischtest, Datenverlaufstabelle, Anweisungsüberdeckung, Fehlersuche, Debugger, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (Bearbeitungszeit insgesamt 111 min, mehrere Aufgaben): Drei umfangreiche Übungsaufgaben: Du bildest Äquivalenzklassen, schreibst parametrisierte Tests mit JUnit und pytest, wertest Testberichte und Fehlermeldungen aus, entscheidest, ob Code oder Test falsch ist, beurteilst Tests nach den F.I.R.S.T.-Regeln, verfolgst eine rekursive Methode, vergleichst zwei Umsetzungen anhand von Testfällen, hinterfragst die Aussagekraft der Anweisungsüberdeckung und suchst Fehler in kurzen Python-Skripten – auch mit Datenverlaufstabelle, Debugger und testgetriebener Entwicklung. Alle Lösungen sind ausgeblendet und mit Punkteverteilung versehen [AI-Assisted]. (Zuletzt geändert am 28.10.2026)

Diese Übungsaufgaben gehören zum Artikel Unit-Tests, Testfälle und Fehlersuche im Code. Weitere Übungen zur Testfallerstellung findest Du im Artikel Mit leeren Händen testen: Blackbox-Testfälle.

Wortwolke mit den Themen der Übungsaufgaben: Unit-Test, Testfall, JUnit, pytest, Äquivalenzklassen, Grenzwerte, Schreibtischtest, Datenverlaufstabelle, Fehlersuche und Debugger (mit KI erstellt)

Hinweise zur Bearbeitung: Als FIA, FISY, FIDP, FIDV, IKS oder IKD musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1, Aufgabe 2.2 und Aufgabe 2.3. Der Quellcode der Aufgaben 2.1 und 2.2 ist in Java geschrieben, der von Aufgabe 2.3 in Python; die Tests nutzen JUnit in der Schreibweise ab Version 5 (assertEquals(erwartet, tatsaechlich), assertThrows(...), @ParameterizedTest) bzw. pytest. Die Zeilennummern am Rand gehören nicht zum Code. Andere fachlich korrekte Lösungen sind ebenfalls richtig. Schreib Deine Lösung zuerst vollständig auf, bevor Du die Beispiellösung aufklappst.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „Unit-Tests, Testfällen und
Fehlersuche im Code“.

Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.

Fachliche Grundlage:
- https://oer-informatik.de/unit-tests
- https://oer-informatik.de/blackboxtests
- https://oer-informatik.de/codecoverage
- https://oer-informatik.de/java-junit-mockito
- https://oer-informatik.de/python_einstieg_pytest
- Stilbeispiel: https://oer-informatik.de/unit-tests-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Unit-Tests
- JUnit
- Parametrisierte Tests
- Testfälle
- Äquivalenzklassen
- Schreibtischtest
- Datenverlaufstabelle
- Anweisungsüberdeckung
- Fehlersuche
- Debugger

Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 2.1: Kostenaufteilung bei Freizeitaktivitäten testen (Kernkompetenz)

(ca. 34 min Bearbeitungszeit)

Du bist Auszubildende*r im Entwicklungsteam der Let’s Meet GmbH in Bielefeld. Das Team baut das Backend der Freizeit-App „Let’s Meet“ neu auf (Java mit Spring Boot, MariaDB). Yusuf Demir, Leiter der Entwicklung, hat festgelegt: Keine Methode geht ohne Unit-Tests in den Hauptzweig. Für Aktivitäten mit Kosten – Eintritt in die Kletterhalle, Bootsmiete, Zutaten für den Kochabend – teilt die App die Gesamtkosten künftig auf alle Teilnehmenden auf. Dein Mit-Azubi Jonas Weber hat die Methode dafür geschrieben, Du sollst sie testen.

Für die Methode anteilInCent(int gesamtCent, int personen) gilt:

  • Die Gesamtkosten liegen zwischen 0 und 100.000 Cent (1.000 €), die Zahl der Personen zwischen 2 und 20. Andere Werte führen zu einer IllegalArgumentException.

  • Der Anteil je Person ist gesamtCent / personen, aufgerundet auf den nächsten vollen Cent, damit die Anteile zusammen die Kosten sicher decken.

a) Bevor Du Jonas’ Code ansiehst, leitest Du Testdaten allein aus der Anforderung ab. Lege für beide Parameter die gültigen und ungültigen Äquivalenzklassen fest und nenne zu jeder Klasse die Grenzwerte, die Du testen würdest. (6 Punkte)

Äquivalenzklassen für anteilInCent
Parameter Äquivalenzklasse (Wertebereich) gültig / ungültig zu testende Grenzwerte

Je Klasse mit passenden Grenzwerten 1 P.

Lösung zu den Äquivalenzklassen
Parameter Äquivalenzklasse (Wertebereich) gültig / ungültig zu testende Grenzwerte
gesamtCent kleiner als 0 ungültig -1
gesamtCent 0 bis 100.000 gültig 0 und 100.000
gesamtCent größer als 100.000 ungültig 100.001
personen kleiner als 2 ungültig 1 (zusätzlich 0)
personen 2 bis 20 gültig 2 und 20
personen größer als 20 ungültig 21

Gut überlegt ist eine weitere Unterteilung der gültigen Eingaben nach dem Ergebnis: Kosten, die sich ohne Rest teilen lassen (z. B. 6000 / 4), und Kosten, bei denen aufgerundet werden muss (z. B. 1000 / 3 = 333,33… → 334). Der Wert 0 bei personen ist ein wichtiger Sonderfall, weil er ohne Prüfung zu einer Division durch 0 führt.

b) Schreibe zwei parametrisierte JUnit-Tests: einen mit @CsvSource für gültige Eingaben (mindestens vier Datensätze, darunter Grenzwerte und ein Fall, in dem aufgerundet werden muss) und einen mit @ValueSource und assertThrows für ungültige Personenzahlen. (6 Punkte)

@ParameterizedTest
@CsvSource({ "6000, 4, 1500", "1000, 3, 334", "0, 2, 0", "100000, 20, 5000", "2501, 2, 1251" })
void anteilWirdAufVollenCentAufgerundet(int gesamtCent, int personen, int erwarteterAnteil) {
    assertEquals(erwarteterAnteil, anteilInCent(gesamtCent, personen));
}

@ParameterizedTest
@ValueSource(ints = { 0, 1, 21 })
void ungueltigePersonenzahlWirftException(int personen) {
    assertThrows(IllegalArgumentException.class, () -> anteilInCent(5000, personen));
}

Bewertung: erster Test mit @ParameterizedTest, @CsvSource und passenden Methodenparametern 1 P, mindestens vier sinnvolle Datensätze mit richtig berechnetem erwartetem Anteil 2 P (davon ein Fall mit Aufrunden); zweiter Test mit @ValueSource 1 P, assertThrows mit richtiger Ausnahmeklasse und Lambda 1 P, Werte aus den ungültigen Klassen 1 P. Ein parametrisierter Test meldet jeden Datensatz einzeln – anders als mehrere assertEquals in einer Testmethode, bei denen JUnit nach dem ersten Fehlschlag abbricht.

c) Jonas hat die Methode so umgesetzt:

1  int anteilInCent(int gesamtCent, int personen) {
2      if (gesamtCent < 0 || gesamtCent > 100000) {
3          throw new IllegalArgumentException("Kosten ungültig");
4      }
5      if (personen < 2 && personen > 20) {
6          throw new IllegalArgumentException("Personenzahl ungültig");
7      }
8      return gesamtCent / personen;
9  }

Seine eigenen parametrisierten Tests liefern diesen Bericht (gekürzt; OK bestanden, FEHLER fehlgeschlagen):

anteilWirdAufgerundet(int, int, int)
  OK     [1] 6000, 4, 1500
  FEHLER [2] 1000, 3, 334    expected: <334> but was: <333>
  FEHLER [3] 0, 5, 1         expected: <1> but was: <0>
  OK     [4] 100000, 20, 5000
ungueltigePersonenzahlWirftException(int)      Aufruf: anteilInCent(5000, personen)
  FEHLER [1] 0     Unexpected exception type thrown ==> expected: <java.lang.IllegalArgumentException>
                but was: <java.lang.ArithmeticException>
  FEHLER [2] 1     Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.
  FEHLER [3] 21    Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.

ca) Entscheide für jeden der fünf Fehlschläge, ob der Code oder der Test falsch ist, und begründe Deine Entscheidung mit der Anforderung. (5 Punkte)

Je Fehlschlag 1 P (Entscheidung mit Begründung).

  • [2] 1000, 3: Code falsch. 1000 / 3 = 333,33…; die Anforderung verlangt Aufrunden auf 334. Zeile 8 rechnet mit ganzzahliger Division und schneidet die Nachkommastellen ab.

  • [3] 0, 5: Test falsch. Bei Kosten von 0 Cent ist der Anteil 0 Cent; die Anforderung kennt keinen Mindestbeitrag. Jonas hat den erwarteten Wert nicht aus der Anforderung abgeleitet. Der Datensatz wird zu 0, 5, 0 korrigiert.

  • [1] personen = 0: Code falsch. Die ungültige Personenzahl wird nicht abgefangen; erst die Division in Zeile 8 bricht mit ArithmeticException (Division durch 0) ab.

  • [2] personen = 1 und [3] personen = 21: Code falsch. Beide Werte liegen außerhalb von 2 bis 20, die Methode liefert aber einen Anteil (5000 bzw. 238) statt einer IllegalArgumentException.

cb) Gib die Zeilen an, die Du in Jonas’ Code änderst, jeweils mit Zeilennummer und vollständig korrigierter Zeile. (4 Punkte)

Je Zeile 2 P (1 P Position, 1 P Korrektur).

Korrekturen in anteilInCent
Zeile korrigierte Zeile
5 if (personen < 2 \|\| personen > 20) {
8 return (gesamtCent + personen - 1) / personen;

Ebenso richtig für Zeile 8: return (int) Math.ceil((double) gesamtCent / personen);. Nach der Korrektur von Code und Testdatensatz [3] werden alle Tests erneut ausgeführt (Regressionstest).

cc) Der Fehler in Zeile 5 wurde weder vom Compiler gemeldet noch hat er bei personen = 1 zu einem Absturz geführt. Ordne ihn einer Fehlerart zu und erkläre, warum die Bedingung in Zeile 5 nie erfüllt ist. (3 Punkte)

Es ist ein semantischer (logischer) Fehler: Die Zeile ist gültiges Java, das Programm läuft, liefert aber ein falsches Ergebnis (1 P). Mit && müssten beide Teilbedingungen zugleich wahr sein. Eine Zahl kann aber nicht gleichzeitig kleiner als 2 und größer als 20 sein, deshalb ist der Ausdruck immer falsch und Zeile 6 wird nie ausgeführt (1 P). Gemeint ist „außerhalb des Bereichs“, also eine Oder-Verknüpfung (||). Erst der Laufzeitfehler bei personen = 0 zeigt, dass etwas nicht stimmt – die eigentliche Ursache liegt aber in Zeile 5, nicht in Zeile 8, wo die Ausnahme entsteht (1 P).

d) Jonas möchte für weitere Tests die Entwicklungsdatenbank verwenden, damit „alles echt“ ist. Erläutere, warum Yusuf das für einen Unit-Test ablehnt und welche Art von Test Jonas eigentlich beschreibt. (3 Punkte)

Ein Unit-Test soll nur die Logik einer Methode isoliert prüfen. Mit einer echten Datenbank wäre er langsamer und nicht mehr unabhängig und wiederholbar: Sein Ergebnis hinge vom Datenbestand, von anderen Tests und von der Erreichbarkeit des Servers ab (2 P). Abhängigkeiten werden im Unit-Test deshalb durch Stubs oder Mocks ersetzt. Das Zusammenspiel von Code und Datenbank prüft ein Integrationstest (1 P).

e) In der Testklasse für das Anlegen von Aktivitäten findest Du diesen Ausschnitt. Die Klasse erzeugt service vor jedem Test neu; der Service nimmt nur Aktivitäten an, die mindestens 24 Stunden in der Zukunft beginnen.

static int angelegt = 0;

@Test
void aktivitaetAnlegen() {
    service.anlegen(new Aktivitaet("Bouldern", LocalDateTime.now().plusHours(24)));
    angelegt++;
    System.out.println("Aktivitäten: " + service.anzahl());
}

@Test
void zweiteAktivitaetAnlegen() {
    service.anlegen(new Aktivitaet("Kochabend", LocalDateTime.now().plusHours(24)));
    assertEquals(angelegt + 1, service.anzahl());
}

Beurteile den Ausschnitt anhand der Eigenschaften guter Unit-Tests: Benenne drei Schwächen jeweils mit der verletzten Eigenschaft und schlage eine Verbesserung vor. (6 Punkte)

Je Schwäche 1 P für Beschreibung mit Eigenschaft, 1 P für die Verbesserung.

  • Nicht selbstvalidierend (self-validating): aktivitaetAnlegen() enthält keine Zusicherung, sondern gibt nur einen Text aus. Der Test ist immer grün, ob das Anlegen klappt oder nicht. Verbesserung: assertEquals(1, service.anzahl()); statt der Ausgabe.

  • Nicht unabhängig (independent): zweiteAktivitaetAnlegen() hängt über die statische Variable angelegt davon ab, ob der andere Test vorher lief. JUnit legt keine Reihenfolge fest; je nach Reihenfolge erwartet der Test 1 oder 2, während service.anzahl() wegen des neuen Service immer 1 ist. Verbesserung: keinen gemeinsamen Zustand zwischen Tests; jeder Test baut seine Ausgangslage selbst auf und erwartet einen festen Wert.

  • Nicht wiederholbar (repeatable): LocalDateTime.now().plusHours(24) liegt genau auf der Grenze der Regel. Bis der Service prüft, sind schon einige Millisekunden vergangen, sodass der Test je nach Timing und Rechner mal besteht und mal nicht. Verbesserung: feste Zeitpunkte verwenden, z. B. eine feste Uhr (Clock.fixed(...)) in den Service geben, und die Grenze gezielt mit „24 Stunden“ und „24 Stunden minus 1 Minute“ testen.

Ebenso richtig: sprechendere Namen, die das erwartete Verhalten nennen; je Test genau ein Verhalten.

f) Anh Nguyen aus dem UX-Team testet das Formular für neue Aktivitäten. Die Anforderung lautet nur: „Der Titel ist ein Pflichtfeld mit höchstens 40 Zeichen.“ Anh gibt einen Titel ein, der für sie aus genau 40 Zeichen besteht, das letzte davon ein Emoji (die Flamme, Unicode U+1F525). Die App lehnt ihn als zu lang ab. Erläutere, wie es dazu kommen kann, und beschreibe, wie Du mit dieser Lücke in der Anforderung umgehst. (4 Punkte)

  • Ursache: Java zählt mit length() keine sichtbaren Zeichen, sondern UTF-16-Einheiten. Ein Emoji wie die Flamme (U+1F525) liegt außerhalb des Basisbereichs von Unicode und belegt zwei Einheiten; der Titel hat für Java also 41 „Zeichen“. Emojis mit Hautton oder zusammengesetzte Emojis belegen noch mehr (1 P).

  • Weitere Stelle: Auch die Datenbank muss mitspielen. Emojis brauchen vier Byte; eine MariaDB-Spalte mit dem Zeichensatz utf8mb3 (in älteren Versionen das Standard-utf8) kann sie nicht speichern, nötig ist utf8mb4 (1 P).

  • Vorgehen: Nicht selbst entscheiden, sondern mit Yusuf bzw. Clara Neumann (Product Owner) klären, was ein „Zeichen“ ist – sinnvoll ist meist „sichtbares Zeichen“ (in Java z. B. über codePointCount oder BreakIterator) – und die Festlegung in der Anforderung dokumentieren (1 P).

  • Testfälle ergänzen: 40 Buchstaben (gültig), 39 Buchstaben plus ein Emoji, 41 Buchstaben (ungültig) und ein zusammengesetztes Emoji; zusätzlich ein Test, der den gespeicherten Titel wieder aus der Datenbank liest (Integrationstest) (1 P).

Aufgabe 2.2: Werkstattaufträge, Berichte und Versandkosten im Fahrradshop (Kernkompetenz)

(ca. 38 min Bearbeitungszeit)

Du bist Auszubildende*r in der IT der CSB-Rad GmbH in Bielefeld. Der Onlineshop und die Werkstattsoftware werden in Java weiterentwickelt. Cem Sarıkaya, Geschäftsführer Technik und IT, lässt vor dem nächsten Release mehrere Methoden prüfen, die verschiedene Kolleg*innen geschrieben haben.

a) Bastian Ludwig plant die Werkstatt. Seine Auftragsliste soll Express-Aufträge immer zuerst zeigen; innerhalb der beiden Gruppen stehen die Aufträge nach Abgabedatum, das früheste zuerst. Die übergebene Liste soll dabei unverändert bleiben.

record Auftrag(String nummer, boolean express, LocalDate abgabe) { }

1  List<Auftrag> sortiereAuftraege(List<Auftrag> auftraege) {
2      List<Auftrag> sortiert = new ArrayList<>(auftraege);
3      sortiert.sort(Comparator.comparing(Auftrag::express)
4                              .thenComparing(Auftrag::abgabe));
5      return sortiert;
6  }

Der zugehörige Test schlägt fehl:

@Test
void expressAuftraegeStehenVorn() {
    List<Auftrag> eingang = List.of(
        new Auftrag("W-12", false, LocalDate.of(2026, 11, 3)),
        new Auftrag("W-15", false, LocalDate.of(2026, 11, 5)),
        new Auftrag("W-17", true,  LocalDate.of(2026, 11, 6)),
        new Auftrag("W-18", true,  LocalDate.of(2026, 11, 4)));
    List<String> nummern = sortiereAuftraege(eingang).stream()
                               .map(Auftrag::nummer).toList();
    assertEquals(List.of("W-18", "W-17", "W-12", "W-15"), nummern);
}
expected: <[W-18, W-17, W-12, W-15]> but was: <[W-12, W-15, W-18, W-17]>

aa) Erkläre anhand der Meldung, nach welcher Regel die Methode tatsächlich sortiert, und begründe, woran das liegt. (3 Punkte)

Die Methode stellt die normalen Aufträge (W-12, W-15) vor die Express-Aufträge (W-18, W-17); innerhalb jeder Gruppe stimmt die Reihenfolge nach Abgabedatum (1 P). Comparator.comparing(Auftrag::express) sortiert nach der natürlichen Ordnung von Boolean, und dort gilt false < true (1 P). Aufsteigend sortiert landen die Express-Aufträge (true) deshalb am Ende; die Gruppen sind also genau vertauscht (1 P).

ab) Gib die korrigierten Zeilen 3 und 4 vollständig an. (2 Punkte)

3      sortiert.sort(Comparator.comparing(Auftrag::express, Comparator.reverseOrder())
4                              .thenComparing(Auftrag::abgabe));

Ebenso richtig: Comparator.comparing(Auftrag::express).reversed().thenComparing(Auftrag::abgabe) – reversed() wirkt hier nur auf das erste Kriterium, weil es vor thenComparing steht. Bewertung: Umkehrung des ersten Kriteriums 1 P, zweites Kriterium unverändert aufsteigend 1 P.

ac) Ein Kollege möchte Speicher sparen und ersetzt Zeile 2 durch List<Auftrag> sortiert = auftraege;. Beschreibe, was beim Ausführen des Tests nun passiert, und ordne den Fehler einer Fehlerart zu. Erläutere außerdem, warum die Änderung auch dann falsch ist, wenn eine veränderbare ArrayList übergeben wird. (4 Punkte)

List.of(...) liefert eine unveränderbare Liste. sort versucht, sie umzuordnen, und bricht mit einer UnsupportedOperationException ab; der Test schlägt mit dieser Ausnahme fehl (1 P). Das ist ein Laufzeitfehler: Der Code wird übersetzt, das Programm bricht erst bei der Ausführung ab (1 P).

Bei einer ArrayList läuft die Methode zwar durch, sortiert aber die Liste des Aufrufers um, denn sortiert und auftraege verweisen auf dasselbe Objekt. Das verletzt die Anforderung „übergebene Liste bleibt unverändert“; andere Programmteile, die die ursprüngliche Reihenfolge brauchen, liefern plötzlich andere Ergebnisse (Seiteneffekt) (2 P).

ad) Bastian ergänzt: „Haben zwei Aufträge dasselbe Abgabedatum, soll die kleinere Auftragsnummer zuerst stehen.“ Ergänze die Sortierung und gib einen Testfall (Eingabeliste und erwartete Reihenfolge) an, der ohne Deine Ergänzung fehlschlagen würde. (4 Punkte)

Ergänzung (2 P):

sortiert.sort(Comparator.comparing(Auftrag::express, Comparator.reverseOrder())
                        .thenComparing(Auftrag::abgabe)
                        .thenComparing(Auftrag::nummer));

Testfall (2 P): Eingabe W-21 und W-20, beide nicht express, beide mit Abgabe am 10.11.2026, in dieser Reihenfolge; erwartet: W-20, W-21. Ohne Ergänzung sind beide Aufträge für den Comparator gleich. List.sort sortiert stabil und lässt gleiche Elemente in der Eingabereihenfolge – das Ergebnis wäre W-21, W-20.

Hinweis: Die Auftragsnummern werden als Text verglichen. Das klappt nur, solange alle Nummern gleich viele Ziffern haben („W-9“ käme sonst nach „W-10“).

b) Natalia Wróbel aus der Buchhaltung erstellt einen Jahresbericht. Beträge in ganzen Euro sollen mit Tausenderpunkten ausgegeben werden, z. B. 9.600.000. Die rekursive Methode tausenderPunkte soll eine nicht negative ganze Zahl entsprechend formatieren; für negative Zahlen soll sie eine IllegalArgumentException werfen.

1  String tausenderPunkte(int zahl) {
2      if (zahl < 1000) {
3          return "" + zahl;
4      }
5      return tausenderPunkte(zahl / 1000) + "." + (zahl % 1000);
6  }

Hinweis: / ist bei int die ganzzahlige Division, % liefert den Rest. "" + zahl wandelt die Zahl in eine Zeichenkette um.

Im Probedruck des Berichts steht beim Umsatz der Werkstatt 1.204.6 statt 1.204.006.

ba) Verfolge den Aufruf tausenderPunkte(1204006) über alle Rekursionsebenen. Notiere je Ebene den Wert von zahl, den rekursiven Aufruf und den Rückgabewert, und zeige, an welcher Stelle die Abweichung entsteht. (3 Punkte)

Aufrufe für tausenderPunkte(1204006)
Ebene zahl Aufruf zahl % 1000 Rückgabe
1 1204006 tausenderPunkte(1204) 6 "1.204" + "." + 6 = "1.204.6"
2 1204 tausenderPunkte(1) 204 "1" + "." + 204 = "1.204"
3 1 – (Abbruchbedingung) – "1"

Die Rückgaben werden von unten nach oben eingesetzt. Die Abweichung entsteht auf Ebene 1: Der Rest 6 wird als 6 statt als 006 angehängt (Aufrufe und Werte 2 P, Stelle der Abweichung 1 P).

bb) Teile die gültigen Eingaben in Klassen ein, für die die Methode richtig bzw. falsch formatiert, und gib zu jeder Klasse einen Testwert mit erwartetem Ergebnis an. (4 Punkte)

Je Klasse mit passendem Testwert 1 P.

Klassen für tausenderPunkte
Klasse Testwert erwartet Methode liefert
Zahl unter 1000 (keine Gruppe) 999 999 richtig
alle Dreiergruppen nach der ersten zwischen 100 und 999 48250 48.250 richtig
eine Dreiergruppe zwischen 10 und 99 305040 305.040 falsch: 305.40
eine Dreiergruppe zwischen 0 und 9 2005 2.005 falsch: 2.5

Besonders aufschlussreich sind die Grenzwerte 1000 (1.000, die Methode liefert 1.0) und 1099 bzw. 1100.

bc) Korrigiere Zeile 5 und ergänze die fehlende Prüfung auf negative Zahlen. (4 Punkte)

Den Rest immer dreistellig mit führenden Nullen ausgeben (2 P):

5      return tausenderPunkte(zahl / 1000) + "." + String.format("%03d", zahl % 1000);

Alternativ: Nullen von Hand ergänzen (rest < 10 → "00", rest < 100 → "0" voranstellen).

Prüfung am Anfang der Methode (2 P):

if (zahl < 0) {
    throw new IllegalArgumentException("Betrag negativ");
}

Ohne diese Prüfung gibt die Methode z. B. für -2500 den Text "-2500" zurück, weil -2500 < 1000 gilt.

bd) Natalia fragt, warum Du die Methode nicht einfach von Hand am Schreibtisch nachrechnest, statt Tests zu schreiben. Erkläre, wie sich ein Schreibtischtest von einem Unit-Test unterscheidet, und wann welcher sinnvoll ist. (2 Punkte)

Beim Schreibtischtest führt ein Mensch den Algorithmus mit Testdaten gedanklich bzw. auf Papier aus (z. B. mit Datenverlaufstabelle); er hilft, einen Fehler zu verstehen, und funktioniert auch für Pseudocode und Entwürfe, ist aber aufwendig und fehleranfällig (1 P). Ein Unit-Test führt den echten Code automatisch aus und vergleicht selbst mit dem erwarteten Ergebnis; er lässt sich nach jeder Änderung beliebig oft wiederholen (1 P).

c) Für den Checkout gelten laut Cem diese Versandregeln:

  • Räder werden per Spedition verschickt: 49,00 € je Rad.

  • Alle übrigen Artikel kommen in ein Paket: Pauschale 6,90 €. Ab einem Warenwert dieser Paketartikel von 100,00 € entfällt die Pauschale. Enthält die Bestellung keine Paketartikel, fällt keine Pauschale an.

Eine Position liefert mit istRad(), ob es sich um ein Rad handelt, mit getMenge() die Menge und mit getEinzelpreis() den Bruttopreis je Stück. Lea und Tim haben unabhängig voneinander eine Methode geschrieben:

// Variante L (Lea)
double versandL(List<Position> positionen) {
    double versand = 0.0;
    double warenwert = 0.0;
    for (Position p : positionen) {
        if (p.istRad()) {
            versand = versand + 49.00 * p.getMenge();
        } else {
            warenwert = warenwert + p.getEinzelpreis();
        }
    }
    if (warenwert > 0 && warenwert < 100.00) {
        versand = versand + 6.90;
    }
    return versand;
}
// Variante T (Tim)
double versandT(List<Position> positionen) {
    int raeder = 0;
    double warenwert = 0.0;
    for (Position p : positionen) {
        if (p.istRad()) {
            raeder = raeder + p.getMenge();
        }
        warenwert = warenwert + p.getMenge() * p.getEinzelpreis();
    }
    double versand = raeder * 49.00;
    if (warenwert > 0 && warenwert < 100.00) {
        versand = versand + 6.90;
    }
    return versand;
}
Testfälle für die Versandkosten
Test Bestellung (Artikel, Menge × Einzelpreis) erwartete Versandkosten
T1 E-Bike „Kanal“: 1 × 2.999,00 €
T2 Trinkflasche: 3 × 12,00 €
T3 Packtasche: 2 × 55,00 €
T4 Stadtrad „Promenade“: 2 × 799,00 €
Klingel: 1 × 9,50 €
T5 Fahrradhelm: 1 × 100,00 €

ca) Ermittle aus den Versandregeln die erwarteten Versandkosten für T1 bis T5. (5 Punkte)

Je Testfall 1 P.

Lösung zu den erwarteten Versandkosten
Test Begründung erwartete Versandkosten
T1 ein Rad, keine Paketartikel 49,00 €
T2 Paketwarenwert 36,00 € < 100,00 € 6,90 €
T3 Paketwarenwert 110,00 € ≥ 100,00 € 0,00 €
T4 2 · 49,00 € + Pauschale (Paketwarenwert 9,50 €) 104,90 €
T5 Paketwarenwert genau 100,00 €, Pauschale entfällt 0,00 €

cb) Jede Variante besteht vier der fünf Tests. Bestimme für jede Variante den fehlschlagenden Test, das tatsächliche Ergebnis und die Ursache im Code. (4 Punkte)

Je Variante 1 P für Test mit tatsächlichem Ergebnis, 1 P für die Ursache.

  • Variante L scheitert an T3: Ergebnis 6,90 € statt 0,00 €. Beim Warenwert wird nur getEinzelpreis() addiert, die Menge fehlt; die zwei Packtaschen zählen nur mit 55,00 €. Bei T2 fällt der Fehler nicht auf, weil 12,00 € und 36,00 € beide unter 100,00 € liegen.

  • Variante T scheitert an T4: Ergebnis 98,00 € statt 104,90 €. Der Warenwert wird für jede Position addiert, also auch für die Räder (fehlender else-Zweig). Der Paketwarenwert erscheint dadurch mit 1.607,50 € statt 9,50 €, und die Pauschale entfällt.

cc) Lea meint: „Beide Varianten bestehen gleich viele Tests, also ist es egal, welche wir nehmen.“ Nimm Stellung, entscheide Dich für eine Variante und gib die Korrektur an, mit der sie alle fünf Tests besteht. (4 Punkte)

Die Zahl bestandener Tests sagt nichts darüber, wie schwer ein Fehler wiegt oder wie oft er auftritt. Leas Fehler trifft Bestellungen, bei denen erst die Menge den Paketwarenwert auf 100 € oder mehr hebt: Kund*innen zahlen dann eine Pauschale, die nicht anfällt. Tims Fehler trifft jede Bestellung mit Rad und Paketartikeln unter 100 €: Der Shop verschenkt die Pauschale. Entscheidend ist, wie häufig die Fälle sind und welcher Schaden größer ist; beide Fehler müssen vor dem Release behoben werden (2 P).

Beide Varianten lassen sich mit einer Änderung korrigieren (2 P für eine davon):

  • Variante L: warenwert = warenwert + p.getMenge() * p.getEinzelpreis();

  • Variante T: den Warenwert nur im else-Zweig addieren:

    if (p.istRad()) {
        raeder = raeder + p.getMenge();
    } else {
        warenwert = warenwert + p.getMenge() * p.getEinzelpreis();
    }

Jede begründete Entscheidung ist richtig. Danach werden alle Tests erneut ausgeführt (Regressionstest).

cd) Schon die Tests T1 und T2 führen jede Anweisung der fehlerhaften Variante L mindestens einmal aus. Weise das nach und erkläre, was das über die Aussagekraft der Anweisungsüberdeckung verrät. (3 Punkte)

T1 durchläuft die Initialisierungen, die Schleife mit dem if-Zweig (Rad) und die Rückgabe; die Pauschalen-Bedingung ist falsch. T2 durchläuft den else-Zweig und, weil der Warenwert zwischen 0 und 100 € liegt, auch die Anweisung versand = versand + 6.90;. Zusammen ist damit jede Anweisung mindestens einmal ausgeführt: 100 % Anweisungsüberdeckung (2 P).

Trotzdem bestehen beide Tests, der Fehler bleibt unentdeckt. Anweisungsüberdeckung zeigt nur, welcher Code ausgeführt wurde, nicht, ob er mit den richtigen Daten geprüft wurde. Den Fehler deckt erst ein Testfall mit Menge > 1 auf, dessen Warenwert die Grenze von 100 € erreicht (T3) (1 P).

Aufgabe 2.3: Skripte für die Barcamp-Technik prüfen (Kernkompetenz)

(ca. 39 min Bearbeitungszeit)

Das Ratz und Rübe Berufskolleg in Bielefeld richtet am 13. März 2027 ein Barcamp aus. Emre Aksoy, Auszubildender Fachinformatiker Systemintegration und Leiter des Technikteams, hat mehrere kleine Python-Skripte für Stromplanung, Infomonitore und E-Ink-Türschilder geschrieben. Bevor sie am Veranstaltungstag laufen, prüfst Du sie. Die Zeilennummern am Rand gehören nicht zum Code.

a) Für jede Steckdosenleiste soll geprüft werden, ob die angeschlossenen Geräte zusammen höchstens die zulässige Leistung aufnehmen. Die Funktion pruefe_last erhält eine Liste mit den Leistungen der Geräte in Watt und den Grenzwert und soll True zurückgeben, wenn die Summe den Grenzwert nicht überschreitet.

1  def pruefe_last(leistungen, grenze_w):
2      summe = 0
3      for i in range(1, len(leistungen)):
4          summe = summe + leistungen[i]
5      if summe < grenze_w:
6          return True
7      return False

aa) Führe für den Aufruf pruefe_last([120, 120, 60, 350], 650) eine Datenverlaufstabelle mit den Spalten i, leistungen[i] und summe. Gib das tatsächliche und das erwartete Ergebnis an. (5 Punkte)

Datenverlaufstabelle für pruefe_last([120, 120, 60, 350], 650)
Schritt i leistungen[i] summe
Start – – 0
1 1 120 120
2 2 60 180
3 3 350 530

530 < 650 ist wahr – tatsächliches Ergebnis True. Erwartet: 120 + 120 + 60 + 350 = 650 W, das ist höchstens 650 W, also ebenfalls True.

Das Ergebnis stimmt nur zufällig: Die Funktion rechnet mit 530 W statt 650 W. Ein einzelner erfolgreicher Testfall beweist also nicht, dass der Code richtig ist.

Bewertung: drei Zeilen der Tabelle je 1 P, tatsächliches und erwartetes Ergebnis zusammen 2 P.

ab) Die Funktion enthält zwei semantische Fehler. Nenne die betroffenen Zeilen, beschreibe jeweils den Fehler und gib die korrigierte Zeile an. (6 Punkte)

  • Zeile 3: Die Schleife beginnt bei Index 1, das erste Gerät wird nie addiert. range(start, ende) läuft in Python von start bis ausschließlich ende; der erste Index ist 0.

    3      for i in range(0, len(leistungen)):

    Ebenso richtig: for leistung in leistungen: oder summe = sum(leistungen).

  • Zeile 5: „Höchstens“ schließt den Grenzwert ein; < liefert bei genau 650 W fälschlich False.

    5      if summe <= grenze_w:

Bewertung: je Fehler Zeile 1 P, Beschreibung 1 P, korrigierte Zeile 1 P.

ac) Gib zwei Testfälle (Eingabe und erwartetes Ergebnis) an, von denen jeder genau einen der beiden Fehler aufdeckt. Begründe kurz. (4 Punkte)

Testfälle, die je einen Fehler aufdecken
Eingabe erwartet tatsächlich (fehlerhafter Code) deckt auf
pruefe_last([700], 650) False True (Schleife läuft gar nicht, summe bleibt 0) Startindex in Zeile 3
pruefe_last([0, 650], 650) True False (650 < 650 ist falsch) Vergleich in Zeile 5

Beim zweiten Testfall ist der erste Wert 0, damit der Startindex das Ergebnis nicht beeinflusst und nur der Grenzwert geprüft wird. Andere Testfälle mit derselben Wirkung sind richtig, z. B. [400, 300] mit Grenze 650 (erwartet False, tatsächlich True).

Bewertung: je Testfall Eingabe und erwartetes Ergebnis 1 P, Begründung 1 P.

b) Auf den zehn Infomonitoren wechseln die Folien alle 20 Sekunden. Nach der vierten Folie bricht das Skript ab:

 1  folien = ["Begrüßung", "Raumplan", "Slot 1", "Slot 2"]
 2  aktuell = 0
 3  while True:
 4      zeige(folien[aktuell])
 5      warte(20)
 6      aktuell = aktuell + 1
Traceback (most recent call last):
  File "monitor.py", line 4, in <module>
IndexError: list index out of range

Erläutere, was die Fehlermeldung bedeutet und wodurch sie entsteht. Entwickle einen Korrekturvorschlag, mit dem die Folien endlos wiederholt werden. (5 Punkte)

Es handelt sich um einen Laufzeitfehler: Das Programm lässt sich starten, bricht aber bei der Ausführung von Zeile 4 mit einer Ausnahme ab. IndexError: list index out of range bedeutet, dass auf einen Index zugegriffen wird, den die Liste nicht hat. Die Liste hat vier Elemente mit den Indizes 0 bis 3; nach der vierten Folie ist aktuell gleich 4 (2 P).

Ursache ist Zeile 6: aktuell wird ohne Obergrenze erhöht (1 P).

Korrektur mit dem Modulo-Operator, der nach dem letzten Index wieder bei 0 beginnt (2 P):

 6      aktuell = (aktuell + 1) % len(folien)

Ebenso richtig: if aktuell == len(folien): aktuell = 0 nach der Erhöhung. len(folien) statt der festen Zahl 4 sorgt dafür, dass neue Folien ohne Codeänderung mitlaufen.

c) Ein weiteres Skript zeigt auf den E-Ink-Türschildern den Titel der laufenden Session. Den Plan liest es aus der Datei plan.csv:

raum;beginn;ende;titel
B204;9:30;10:20;KI im Unterricht – kritisch und praxisnah
B204;10:35;11:20;Barrierefreie Websites bauen
1  import csv
2
3  def aktuelle_session(datei, raum, uhrzeit):
4      with open(datei, encoding="utf-8") as f:
5          for zeile in csv.DictReader(f, delimiter=";"):
6              if zeile["Raum"] == raum and zeile["beginn"] <= uhrzeit < zeile["ende"]:
7                  return zeile["titel"]
8      return "Pause"

ca) Der erste Aufruf aktuelle_session("plan.csv", "B204", "9:45") bricht mit KeyError: 'Raum' ab. Erläutere die Meldung und gib die korrigierte Zeile an. (2 Punkte)

csv.DictReader verwendet die Kopfzeile der Datei als Schlüssel des Dictionarys. Einen Schlüssel "Raum" gibt es nicht, nur "raum"; Python unterscheidet Groß- und Kleinschreibung. Der Zugriff auf einen fehlenden Schlüssel löst einen KeyError aus (1 P).

6              if zeile["raum"] == raum and zeile["beginn"] <= uhrzeit < zeile["ende"]:

Korrigierte Zeile 1 P.

cb) Nach dieser Korrektur liefert derselbe Aufruf "Pause", obwohl um 9:45 Uhr in B204 „KI im Unterricht“ läuft. Erkläre die Ursache, indem Du die Vergleiche in Zeile 6 für beide Sessions auswertest, und schlage zwei verschiedene Korrekturen vor. (4 Punkte)

Die Uhrzeiten sind Zeichenketten und werden deshalb Zeichen für Zeichen verglichen, nicht als Zeitangaben (1 P):

  • Session 1: "9:30" <= "9:45" ist wahr, aber "9:45" < "10:20" ist falsch, weil schon das erste Zeichen entscheidet: "9" kommt nach "1".

  • Session 2: "10:35" <= "9:45" ist wahr ("1" vor "9"), "9:45" < "11:20" ist falsch.

Keine Bedingung ist vollständig erfüllt, die Funktion gibt "Pause" zurück (1 P).

Korrekturen (je 1 P):

  • Daten vereinheitlichen: Alle Uhrzeiten zweistellig schreiben (09:30, Format HH:MM), sowohl in der Datei als auch beim Aufruf. Dann stimmt die Reihenfolge der Zeichenketten mit der zeitlichen überein. Das ist aber fehleranfällig, weil jede neue Eingabe sich daran halten muss.

  • Als Zeit vergleichen: Die Texte in Zeitwerte umwandeln, z. B. mit datetime.strptime(text, "%H:%M").time(), und erst dann vergleichen. Das funktioniert auch mit 9:30.

cc) Ordne beide Fehler einer Fehlerart zu und begründe, welcher der beiden ohne gezielten Test leichter unbemerkt geblieben wäre. (2 Punkte)

Der KeyError ist ein Laufzeitfehler: Das Skript startet, bricht aber bei der Ausführung von Zeile 6 ab. Der Vergleich von Zeichenketten ist ein semantischer Fehler: Das Skript läuft ohne Meldung, liefert aber ein falsches Ergebnis (1 P).

Leichter übersehen wird der semantische Fehler: Für Sessions ab 10 Uhr stimmt das Ergebnis (z. B. "10:45" → „Barrierefreie Websites bauen“). Er fällt nur mit einem Testfall vor 10 Uhr bzw. über die Grenze 9:59 / 10:00 auf (1 P).

d) Im Technikteam sollen die Schüler*innen lernen, Fehler mit dem Debugger zu suchen.

da) Am Beispiel der ursprünglichen Funktion pruefe_last aus a) sollen die Schüler*innen sehen, wie man mit dem Debugger den Fehler in Zeile 3 eingrenzt. Beschreibe Schritt für Schritt, wie Du dabei vorgehst: wo Du einen Haltepunkt setzt, welche Werte Du beobachtest, wie Du Dich durch den Code bewegst und woran Du den Fehler erkennst. (4 Punkte)

Je sinnvollem Schritt 1 P, z. B.:

  1. Fehlschlagenden Fall wählen: Die Funktion mit pruefe_last([700], 650) aufrufen, erwartet ist False.

  2. Haltepunkt setzen: auf Zeile 3 (Schleifenkopf) bzw. Zeile 4 und das Programm im Debug-Modus starten.

  3. Werte beobachten: i, leistungen[i] und summe in die Beobachtungsliste (watch) aufnehmen bzw. in der Variablenansicht verfolgen.

  4. Schrittweise ausführen: mit step over Zeile für Zeile weitergehen. Der Debugger springt von Zeile 3 direkt zu Zeile 5, Zeile 4 wird nie erreicht; summe bleibt 0. Bei der Liste [120, 120, 60, 350] zeigt sich entsprechend, dass i bei 1 beginnt und der erste Wert fehlt.

Der Debugger zeigt so, wo der tatsächliche Ablauf vom erwarteten abweicht; dass False erwartet ist, weißt Du aber nur aus der Anforderung.

db) Emre meint: „Mit dem Debugger finde ich jeden Fehler, Tests brauchen wir für die Skripte nicht.“ Nimm Stellung. (3 Punkte)

Die Aussage trifft nicht zu. Der Debugger zeigt, was das Programm tut, aber nicht, was es tun soll: Einen semantischen Fehler wie < statt <= erkennst Du nur, wenn Du das erwartete Ergebnis kennst – genau das hält ein Testfall fest (1 P). Der Debugger untersucht außerdem nur den Fall, den Du gerade ausführst; Unit-Tests prüfen viele Fälle automatisch und nach jeder Änderung erneut (Regressionstest) (1 P). Fehlende Anforderungen, z. B. ein nicht behandelter Sonderfall, findet er ebenfalls nicht. Sinnvoll ist beides: Tests zeigen, dass ein Fehler vorliegt, der Debugger hilft, ihn einzugrenzen (1 P).

e) Emre möchte die korrigierte Funktion pruefe_last künftig mit pytest automatisch testen. Schreibe einen parametrisierten Test mit @pytest.mark.parametrize, der die beiden Testfälle aus ac), die Grenze selbst und den Wert direkt darüber sowie eine leere Liste enthält. Gib zu jedem Datensatz das erwartete Ergebnis an. (5 Punkte)

import pytest
from strom import pruefe_last

@pytest.mark.parametrize("leistungen, grenze_w, erwartet", [
    ([700], 650, False),                 # nur ein Gerät, über der Grenze
    ([0, 650], 650, True),               # genau auf der Grenze
    ([120, 120, 60, 350], 650, True),    # Summe genau 650 W
    ([120, 120, 60, 351], 650, False),   # 1 W über der Grenze
    ([], 650, True),                     # keine Geräte angeschlossen
])
def test_pruefe_last(leistungen, grenze_w, erwartet):
    assert pruefe_last(leistungen, grenze_w) == erwartet

Bewertung: Dekorator mit Parameternamen und Liste von Datensätzen 1 P, Testfunktion mit passenden Parametern und assert 1 P, Testfälle aus ac) 1 P, Grenzwert und Wert direkt darüber 1 P, leere Liste mit erwartetem Ergebnis True (Summe 0 W) 1 P. pytest meldet jeden Datensatz als eigenen Testfall.

f) Für die nächsten Skripte möchte Emre testgetrieben entwickeln. Beschreibe die Idee der testgetriebenen Entwicklung (TDD) und erläutere, wie die beiden Testfälle aus ac) dabei eingesetzt würden. (3 Punkte)

Bei TDD wird der Test vor dem Code geschrieben, in kurzen Zyklen: Zuerst einen Test schreiben, der fehlschlägt (Red), dann gerade so viel Code, dass er erfolgreich ist (Green), danach den Code aufräumen, ohne dass ein Test rot wird (Refactor) (2 P).

Für pruefe_last hätte Emre die beiden Testfälle aus ac) als automatisierte Tests zuerst formuliert, z. B. assert pruefe_last([700], 650) is False und assert pruefe_last([0, 650], 650) is True. Die fehlerhafte Fassung wäre dann sofort rot gewesen; nach der Korrektur bleiben die Tests als Regressionstests erhalten (1 P).


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu Unit-Tests, Testfällen und Fehlersuche im Code” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/unit-tests-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/qs/testen. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 28.10.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: