ProjektBaut auf Katas: Listen auf
image: teaser.webp image_alt: "VS Code mit einer Testdatei für den Notenrechner; im Terminal darunter scheitert ein Test: erwartet 1.0, tatsächlich 1.1." kind: projekt duration: 2 Doppelstunden languages: [ruby, python, javascript] curriculum: "Oberstufe: Softwareprojekt (systematische Softwareentwicklung: Testen)" topics: [testen] requires: [kata-lists]

Testen: der Notenrechner

Ein Notenrechner für die Oberstufe sagt bei 805 Punkten eine 1,2 voraus, im Zeugnis steht später eine 1,1. Du schreibst Tests, die solche Fehler finden, mit dem Testwerkzeug deiner Sprache, und lernst, wo sich Fehler verstecken: an den Grenzen. Am Ende schreibst du für eine neue Funktion zuerst die Tests und dann den Code.

1. Die Geschichte

Ein Abiturjahrgang rechnet mit einem kleinen Programm aus, welcher Schnitt sich aus den Punkten ergibt. Eine Schülerin kommt auf 805 Punkte, und das Programm sagt 1,2. Im Zeugnis steht später 1,1.

Wer recht hat, steht in der Vereinbarung der Kultusministerkonferenz zur gymnasialen Oberstufe, Anlage 4: eine Formel für die Durchschnittsnote \(N\) aus den Punkten \(E\) und eine Tabelle, die für jede Punktzahl die Note nennt. Das Programm hat sich an die Formel gehalten und trotzdem eine falsche Note ausgerechnet. Einen solchen Fehler findet man nicht, indem man den Code liest und nickt, sondern mit Tests: kleinen Programmen, die den Code mit Werten aufrufen, deren Ergebnis man kennt, und vergleichen.

2. Einrichten

Lade das Programm in einen neuen Ordner notenrechner:

mkdir -p ~/notenrechner
cd ~/notenrechner
wget https://workspace.hackschule.de/testen/noten.rb

Öffne den Ordner mit StrgK StrgO. In noten.rb stehen drei Methoden: grade macht aus 0 bis 15 Punkten eine Note, deficit? sagt, ob ein Kurs mit so vielen Punkten ein Defizit ist, und abitur_grade rechnet die Durchschnittsnote im Abitur aus. Probier sie im Terminal aus:

ruby -r ./noten -e 'puts abitur_grade(805)'

Lade das Programm in einen neuen Ordner notenrechner:

mkdir -p ~/notenrechner
cd ~/notenrechner
wget https://workspace.hackschule.de/testen/noten.py

Öffne den Ordner mit StrgK StrgO. In noten.py stehen drei Funktionen: grade macht aus 0 bis 15 Punkten eine Note, is_deficit sagt, ob ein Kurs mit so vielen Punkten ein Defizit ist, und abitur_grade rechnet die Durchschnittsnote im Abitur aus. Probier sie im Terminal aus:

python3 -c 'from noten import abitur_grade; print(abitur_grade(805))'

Lade das Programm in einen neuen Ordner notenrechner:

mkdir -p ~/notenrechner
cd ~/notenrechner
wget https://workspace.hackschule.de/testen/noten.js

Öffne den Ordner mit StrgK StrgO. In noten.js stehen drei Funktionen: grade macht aus 0 bis 15 Punkten eine Note, isDeficit sagt, ob ein Kurs mit so vielen Punkten ein Defizit ist, und abiturGrade rechnet die Durchschnittsnote im Abitur aus. Am Ende gibt module.exports sie heraus, damit andere Dateien sie mit require holen können. Probier sie im Terminal aus:

node -e 'console.log(require("./noten").abiturGrade(805))'

Da steht die 1,2 aus der Geschichte. Ob das stimmt, klärst du später; zuerst lernst du, wie ein Test aussieht.

3. Ein erster Test

Ruby bringt ein Testwerkzeug mit, minitest. Ein Test ist eine Methode, deren Name mit test_ beginnt, in einer Klasse, die von Minitest::Test erbt. Darin vergleicht assert_equal einen erwarteten Wert mit dem, was der Code liefert, zuerst den erwarteten. Leg im Ordner notenrechner die Datei test_noten.rb an:

require 'minitest/autorun'
require_relative 'noten'

class NotenTest < Minitest::Test
  def test_grade_at_the_top
    assert_equal '1+', grade(15)
  end
end

require 'minitest/autorun' sorgt dafür, dass die Tests laufen, sobald du die Datei startest; require_relative 'noten' holt die Methoden aus noten.rb. Starte sie im Terminal:

ruby test_noten.rb

Für jeden bestandenen Test steht ein Punkt in der Ausgabe, und am Ende zählt minitest: 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips.

Für Python gibt es das Testwerkzeug pytest. Ein Test ist eine Funktion, deren Name mit test_ beginnt, in einer Datei, deren Name mit test_ beginnt. Darin prüft assert, ob ein Vergleich stimmt. Leg im Ordner notenrechner die Datei test_noten.py an:

from noten import grade


def test_grade_at_the_top():
    assert grade(15) == "1+"

Starte pytest im Terminal, im Ordner notenrechner:

pytest

pytest sucht sich die Dateien und Funktionen, deren Name mit test_ beginnt, führt sie aus und zählt am Ende: 1 passed.

Node bringt ein Testwerkzeug mit, node --test. Ein Test ist ein Aufruf von test mit einem Namen und einer Funktion. Darin vergleicht assert.strictEqual den Wert, den der Code liefert, mit dem erwarteten. Leg im Ordner notenrechner die Datei test_noten.js an:

const test = require('node:test');
const assert = require('node:assert');
const { grade } = require('./noten');

test('grade at the top', () => {
  assert.strictEqual(grade(15), '1+');
});

Starte sie im Terminal:

node --test test_noten.js

Für jeden bestandenen Test steht ein Haken mit seinem Namen in der Ausgabe, und am Ende zählt Node: tests 1, pass 1, fail 0.

Aufgabe 1Dein erster Test

Schreib in notenrechner/test_noten.rb den Test von oben, der prüft, dass 15 Punkte die Note 1+ sind, und lass ihn mit ruby test_noten.rb laufen. Die Prüfung führt deinen Test mit einem richtigen noten.rb aus und mit einem, das für jede Punktzahl die Note eines Punkts weniger liefert.

Tipp

Die Datei beginnt mit zwei Zeilen require, dann kommt eine Klasse, die von Minitest::Test erbt, mit einer Methode, deren Name mit test_ beginnt.

Nächster Tipp

In der Methode steht eine Zeile assert_equal '1+', grade(15), zuerst der erwartete Wert, dann der Aufruf.

Aufgabe 1Dein erster Test

Schreib in notenrechner/test_noten.py den Test von oben, der prüft, dass 15 Punkte die Note 1+ sind, und lass ihn mit pytest laufen. Die Prüfung führt deinen Test mit einem richtigen noten.py aus und mit einem, das für jede Punktzahl die Note eines Punkts weniger liefert.

Tipp

Die Datei holt sich grade mit from noten import grade, dann kommt eine Funktion, deren Name mit test_ beginnt.

Nächster Tipp

In der Funktion steht eine Zeile assert grade(15) == "1+".

Aufgabe 1Dein erster Test

Schreib in notenrechner/test_noten.js den Test von oben, der prüft, dass 15 Punkte die Note 1+ sind, und lass ihn mit node --test test_noten.js laufen. Die Prüfung führt deinen Test mit einem richtigen noten.js aus und mit einem, das für jede Punktzahl die Note eines Punkts weniger liefert.

Tipp

Die Datei holt sich test, assert und grade mit require, dann kommt ein Aufruf test('…', () => { … }).

Nächster Tipp

In der Funktion steht eine Zeile assert.strictEqual(grade(15), '1+');, zuerst der Aufruf, dann der erwartete Wert.

Ein Test, der besteht, sagt wenig, solange du nicht weißt, wie er aussieht, wenn er scheitert.

Vermutung 2Ein Test, der scheitert

Angenommen, du änderst in deinem Test die erwartete Note von 1+ zu 1 und lässt ihn noch einmal laufen. Was passiert?

Ein Test, der scheitert

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Der Test scheitert und sagt, was er erwartet hat, was herauskam und in welcher Zeile. Wer recht hat, entscheidet das Testwerkzeug nicht: Diesmal ist der Test falsch, nicht grade. Ein scheiternder Test heißt rot, ein bestandener grün, nach den Farben, in denen viele Werkzeuge sie anzeigen.

Das Terminal nach ruby test_noten.rb: ein F statt eines Punkts, dann ein Failure in NotenTest#test_grade_at_the_top, test_noten.rb:6, mit Expected: "1" und Actual: "1+", und am Ende 1 runs, 1 assertions, 1 failures.

Das Terminal nach pytest: test_noten.py F, dann unter FAILURES der Test test_grade_at_the_top mit der Zeile assert grade(15) == "1" und darunter AssertionError: assert '1+' == '1', die Zeile test_noten.py:5 und am Ende 1 failed.

Das Terminal nach node --test test_noten.js: ein rotes Kreuz bei grade at the top, die Zählung tests 1, pass 0, fail 1, dann der gescheiterte Test mit AssertionError, '1+' !== '1', der Zeile test_noten.js:6 und actual '1+', expected '1'.

Setz die Erwartung danach wieder auf 1+.

4. Wo sich Fehler verstecken

Klassen und Grenzen

Alle Eingaben auszuprobieren geht selten. Bei grade wären es nur 16 Punktzahlen und dazu alles, was keine ist; bei der Durchschnittsnote schon 601, und eine Liste von 36 Ergebnissen hat mehr Möglichkeiten, als es Sekunden seit dem Urknall gibt. Man teilt die Eingaben darum in Klassen, die das Programm gleich behandelt. Eine Funktion, die sagt, ob jemand volljährig ist, hat zwei davon: die Alter unter 18 und die ab 18. Für eine Klasse genügt ein Vertreter: Klappt es mit einem, sollte es mit den anderen auch klappen.

Fehler sitzen aber besonders gern dort, wo eine Klasse aufhört und die nächste beginnt: ein < statt <=, eine 17, wo 18 gemeint war. Darum testet man an jeder Grenze beide Seiten, den letzten Wert der einen Klasse und den ersten der anderen, bei der Volljährigkeit also 17 und 18.

Aufgabe 3Die Grenzen von grade

grade nimmt 0 bis 15 Punkte an und meldet für alles andere einen Fehler. An welchen Punktzahlen liegen die Grenzen, an denen sich das Verhalten ändert, auf beiden Seiten?

Die Grenzen von grade
Mehrere Antworten können richtig sein.
Tipp

Wo endet das, was es gibt, und wo beginnt das, was es nicht gibt? Eine Grenze hat zwei Seiten.

Fehler erwarten

Ein Test kann auch prüfen, dass etwas schiefgeht. Gibt es eine Punktzahl nicht, soll grade einen Fehler melden, statt still eine falsche Note zu liefern. So sieht ein Test dafür aus, für einen Text statt einer Zahl:

def test_grade_of_text
  assert_raises(ArgumentError) { grade('zwölf') }
end

assert_raises führt den Block aus und besteht nur, wenn darin ein ArgumentError geworfen wird.

def test_grade_of_text():
    with pytest.raises(ValueError):
        grade("zwölf")

Der Block with pytest.raises(ValueError): besteht nur, wenn darin ein ValueError geworfen wird. Dafür kommt import pytest oben in die Datei.

test('grade of text', () => {
  assert.throws(() => grade('zwölf'), RangeError);
});

assert.throws ruft die Funktion auf und besteht nur, wenn darin ein RangeError geworfen wird.

Jetzt bist du dran: Schreib Tests für grade, in drei Tests: die beiden Enden, ein paar Punktzahlen dazwischen und die Werte gleich neben den Enden, bei denen es einen Fehler geben muss. Lass sie laufen, bis alle grün sind.
Tipp

Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die in der Oberstufe zählt: Darunter ist ein Kurs ein Defizit. Welche Note gehört zu 5 Punkten?

Lösung anzeigen
# die Enden: 0 und 15 Punkte
def test_grade_at_the_ends
  assert_equal '6', grade(0)
  assert_equal '1+', grade(15)
end

def test_grade_in_between
  assert_equal '1-', grade(13)
  assert_equal '4', grade(5)
end

# gleich neben den Enden: Punkte, die es nicht gibt
def test_grade_outside
  assert_raises(ArgumentError) { grade(-1) }
  assert_raises(ArgumentError) { grade(16) }
end
Tipp

Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die in der Oberstufe zählt: Darunter ist ein Kurs ein Defizit. Welche Note gehört zu 5 Punkten?

Lösung anzeigen
# die Enden: 0 und 15 Punkte
def test_grade_at_the_ends():
    assert grade(0) == "6"
    assert grade(15) == "1+"


def test_grade_in_between():
    assert grade(13) == "1-"
    assert grade(5) == "4"


# gleich neben den Enden: Punkte, die es nicht gibt
def test_grade_outside():
    with pytest.raises(ValueError):
        grade(-1)
    with pytest.raises(ValueError):
        grade(16)
Tipp

Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die in der Oberstufe zählt: Darunter ist ein Kurs ein Defizit. Welche Note gehört zu 5 Punkten?

Lösung anzeigen
// die Enden: 0 und 15 Punkte
test('grade at the ends', () => {
  assert.strictEqual(grade(0), '6');
  assert.strictEqual(grade(15), '1+');
});

test('grade in between', () => {
  assert.strictEqual(grade(13), '1-');
  assert.strictEqual(grade(5), '4');
});

// gleich neben den Enden: Punkte, die es nicht gibt
test('grade outside', () => {
  assert.throws(() => grade(-1), RangeError);
  assert.throws(() => grade(16), RangeError);
});
Aufgabe 4Tests für grade

Schreib in notenrechner/test_noten.rb Tests für grade: an beiden Enden, für Punktzahlen dazwischen und gleich neben den Enden, wo es einen Fehler geben muss. Die Prüfung führt sie mit einem richtigen noten.rb aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften, von denen jedes an mindestens einem Test scheitern muss.

Tipp

Drei Arten von Tests: die Enden (0 und 15), etwas dazwischen und das, was es nicht gibt. Schreib für jede Art eine Methode.

Nächster Tipp

Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die für die Oberstufe zählt. Welche Note gehört zu 5 Punkten?

Nächster Tipp

Für die Punkte, die es nicht gibt, steht der Aufruf in einem Block hinter assert_raises(ArgumentError), wie oben bei 'zwölf'.

Aufgabe 4Tests für grade

Schreib in notenrechner/test_noten.py Tests für grade: an beiden Enden, für Punktzahlen dazwischen und gleich neben den Enden, wo es einen Fehler geben muss. Die Prüfung führt sie mit einem richtigen noten.py aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften, von denen jedes an mindestens einem Test scheitern muss.

Tipp

Drei Arten von Tests: die Enden (0 und 15), etwas dazwischen und das, was es nicht gibt. Schreib für jede Art eine Funktion.

Nächster Tipp

Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die für die Oberstufe zählt. Welche Note gehört zu 5 Punkten?

Nächster Tipp

Für die Punkte, die es nicht gibt, steht der Aufruf in einem Block with pytest.raises(ValueError):, wie oben bei "zwölf".

Aufgabe 4Tests für grade

Schreib in notenrechner/test_noten.js Tests für grade: an beiden Enden, für Punktzahlen dazwischen und gleich neben den Enden, wo es einen Fehler geben muss. Die Prüfung führt sie mit einem richtigen noten.js aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften, von denen jedes an mindestens einem Test scheitern muss.

Tipp

Drei Arten von Tests: die Enden (0 und 15), etwas dazwischen und das, was es nicht gibt. Schreib für jede Art einen Aufruf von test.

Nächster Tipp

Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die für die Oberstufe zählt. Welche Note gehört zu 5 Punkten?

Nächster Tipp

Für die Punkte, die es nicht gibt, steht der Aufruf in einer Funktion in assert.throws(() => …, RangeError), wie oben bei 'zwölf'.

Ausprobieren: Bau in grade absichtlich einen Fehler ein, etwa eine 16 statt der 15 in der Prüfung der Punkte, und lass die Tests laufen. Welcher Test wird rot, und welche bleiben grün? Mach die Änderung danach rückgängig.

5. Der Fehler im Abi-Schnitt

Die Tabelle der KMK

Die Durchschnittsnote im Abitur kommt aus den Punkten \(E\) der Gesamtqualifikation, 300 bis 900:

$$N = \frac{17}{3} - \frac{E}{180}$$

Daneben steht in der Vereinbarung eine Tabelle, die für jede Punktzahl die Note nennt. Ein Ausschnitt:

Punkte Note
900 bis 823 1,0
822 bis 805 1,1
804 bis 787 1,2
786 bis 769 1,3
… …
336 bis 319 3,8
318 bis 301 3,9
300 4,0

Jede Note gilt für 18 Punktzahlen, bis auf die 1,0, die bis 900 reicht: Besser als 1,0 gibt es nicht, auch wenn die Formel bei 900 Punkten 0,67 ergibt.

Vermutung 5823 Punkte

Nach der Tabelle der KMK sind 823 Punkte eine 1,0. Was liefert abitur_grade(823) aus noten, so wie die Datei jetzt ist? Rechne mit der Zeile, die dort steht.

823 Punkte
Vermutung 5823 Punkte

Nach der Tabelle der KMK sind 823 Punkte eine 1,0. Was liefert abitur_grade(823) aus noten, so wie die Datei jetzt ist? Rechne mit der Zeile, die dort steht.

823 Punkte
Vermutung 5823 Punkte

Nach der Tabelle der KMK sind 823 Punkte eine 1,0. Was liefert abiturGrade(823) aus noten, so wie die Datei jetzt ist? Rechne mit der Zeile, die dort steht.

823 Punkte

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Die Formel ergibt bei 823 Punkten 1,094 und bei 805 Punkten 1,194. Gerundet auf eine Nachkommastelle sind das 1,1 und 1,2, und genau das tut noten. Die Tabelle schneidet nach der ersten Nachkommastelle ab und kommt auf 1,0 und 1,1. Im Kommentar über der Funktion steht es sogar richtig, »abgeschnitten, nicht gerundet«; der Code darunter tut etwas anderes. Ein Kommentar prüft nichts, ein Test schon.

Tests aus der Tabelle

Jetzt bist du dran: Schreib Tests für die Durchschnittsnote nach der Tabelle: an einer oder zwei Grenzen, auf beiden Seiten, an den Enden und gleich daneben, wo es einen Fehler geben muss. Diesmal werden sie rot, denn noten hat einen Fehler, und dafür sind sie da.
Tipp

823 und 822 liegen auf den beiden Seiten einer Grenze. Zwei Kommazahlen vergleicht assert_equal wie zwei ganze: assert_equal 1.0, abitur_grade(823).

Lösung anzeigen
# an den Grenzen der Tabelle der KMK
def test_abitur_grade_at_the_boundaries
  assert_equal 1.0, abitur_grade(900)
  assert_equal 1.0, abitur_grade(823)
  assert_equal 1.1, abitur_grade(822)
  assert_equal 1.1, abitur_grade(805)
  assert_equal 1.2, abitur_grade(804)
  assert_equal 4.0, abitur_grade(300)
end

def test_abitur_grade_outside
  assert_raises(ArgumentError) { abitur_grade(299) }
  assert_raises(ArgumentError) { abitur_grade(901) }
end
Tipp

823 und 822 liegen auf den beiden Seiten einer Grenze. abitur_grade holst du dir mit from noten import abitur_grade, grade.

Lösung anzeigen
# an den Grenzen der Tabelle der KMK
def test_abitur_grade_at_the_boundaries():
    assert abitur_grade(900) == 1.0
    assert abitur_grade(823) == 1.0
    assert abitur_grade(822) == 1.1
    assert abitur_grade(805) == 1.1
    assert abitur_grade(804) == 1.2
    assert abitur_grade(300) == 4.0


def test_abitur_grade_outside():
    with pytest.raises(ValueError):
        abitur_grade(299)
    with pytest.raises(ValueError):
        abitur_grade(901)
Tipp

823 und 822 liegen auf den beiden Seiten einer Grenze. abiturGrade holst du dir in derselben Zeile wie grade: const { grade, abiturGrade } = require('./noten');.

Lösung anzeigen
// an den Grenzen der Tabelle der KMK
test('abiturGrade at the boundaries', () => {
  assert.strictEqual(abiturGrade(900), 1.0);
  assert.strictEqual(abiturGrade(823), 1.0);
  assert.strictEqual(abiturGrade(822), 1.1);
  assert.strictEqual(abiturGrade(805), 1.1);
  assert.strictEqual(abiturGrade(804), 1.2);
  assert.strictEqual(abiturGrade(300), 4.0);
});

test('abiturGrade outside', () => {
  assert.throws(() => abiturGrade(299), RangeError);
  assert.throws(() => abiturGrade(901), RangeError);
});

Deine Tests sind rot, weil noten falsch rechnet. Die Prüfung der Aufgabe führt sie deshalb nicht mit deinem noten aus, sondern mit einem richtigen, mit dem alle grün sein müssen, und mit fehlerhaften, darunter dem, das rundet.

Aufgabe 6Tests für den Abi-Schnitt

Schreib in notenrechner/test_noten.rb Tests für abitur_grade nach der Tabelle der KMK: an Grenzen der Tabelle, auf beiden Seiten, an beiden Enden und gleich daneben. Mit deinem noten.rb scheitern sie, und das sollen sie auch. Die Prüfung führt sie mit einem richtigen noten.rb aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften, darunter dem, das rundet.

Tipp

Such in der Tabelle eine Grenze, an der eine neue Note beginnt. Welche zwei Punktzahlen liegen direkt an ihr?

Nächster Tipp

Die Enden der Tabelle sind 900 und 300 Punkte; gleich daneben liegen 901 und 299, wo es einen Fehler geben muss.

Nächster Tipp

assert_equal 1.0, abitur_grade(823) prüft eine Grenze von der einen Seite, assert_equal 1.1, abitur_grade(822) von der anderen.

Aufgabe 6Tests für den Abi-Schnitt

Schreib in notenrechner/test_noten.py Tests für abitur_grade nach der Tabelle der KMK: an Grenzen der Tabelle, auf beiden Seiten, an beiden Enden und gleich daneben. Mit deinem noten.py scheitern sie, und das sollen sie auch. Die Prüfung führt sie mit einem richtigen noten.py aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften, darunter dem, das rundet.

Tipp

Such in der Tabelle eine Grenze, an der eine neue Note beginnt. Welche zwei Punktzahlen liegen direkt an ihr?

Nächster Tipp

Die Enden der Tabelle sind 900 und 300 Punkte; gleich daneben liegen 901 und 299, wo es einen Fehler geben muss.

Nächster Tipp

assert abitur_grade(823) == 1.0 prüft eine Grenze von der einen Seite, assert abitur_grade(822) == 1.1 von der anderen.

Aufgabe 6Tests für den Abi-Schnitt

Schreib in notenrechner/test_noten.js Tests für abiturGrade nach der Tabelle der KMK: an Grenzen der Tabelle, auf beiden Seiten, an beiden Enden und gleich daneben. Mit deinem noten.js scheitern sie, und das sollen sie auch. Die Prüfung führt sie mit einem richtigen noten.js aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften, darunter dem, das rundet.

Tipp

Such in der Tabelle eine Grenze, an der eine neue Note beginnt. Welche zwei Punktzahlen liegen direkt an ihr?

Nächster Tipp

Die Enden der Tabelle sind 900 und 300 Punkte; gleich daneben liegen 901 und 299, wo es einen Fehler geben muss.

Nächster Tipp

assert.strictEqual(abiturGrade(823), 1.0) prüft eine Grenze von der einen Seite, assert.strictEqual(abiturGrade(822), 1.1) von der anderen.

Den Fehler beheben

Jetzt bist du dran: Ändere die Durchschnittsnote in noten so, dass sie nach der ersten Nachkommastelle abschneidet, statt zu runden. Lass nach jeder Änderung die Tests laufen, bis alle grün sind.
Tipp

Abschneiden auf eine Nachkommastelle heißt mal 10 rechnen, nach unten runden und wieder durch 10 teilen. Es geht auch ohne Kommazahlen: Die Note in Zehnteln ist (1020 - total) / 18, ganzzahlig geteilt. Prüf das mit einer Punktzahl aus der Tabelle nach.

Lösung anzeigen
def abitur_grade(total)  unless total.between?(300, 900)    raise ArgumentError, "#{total} Punkte: Die Gesamtqualifikation liegt zwischen 300 und 900"  end  # in Zehnteln: (1020 - total) / 18, ganzzahlig geteilt, also abgeschnitten  tenths = (1020 - total) / 18  [tenths, 10].max / 10.0end
Tipp

Abschneiden auf eine Nachkommastelle heißt mal 10 rechnen, nach unten runden und wieder durch 10 teilen. Es geht auch ohne Kommazahlen: Die Note in Zehnteln ist (1020 - total) // 18. Prüf das mit einer Punktzahl aus der Tabelle nach.

Lösung anzeigen
def abitur_grade(total):    if not 300 <= total <= 900:        raise ValueError(f"{total} Punkte: Die Gesamtqualifikation liegt zwischen 300 und 900")    # in Zehnteln: (1020 - total) // 18, ganzzahlig geteilt, also abgeschnitten    tenths = (1020 - total) // 18    return max(tenths, 10) / 10
Tipp

Abschneiden auf eine Nachkommastelle heißt mal 10 rechnen, nach unten runden (Math.floor statt Math.round) und wieder durch 10 teilen. Es geht auch ohne Kommazahlen: Die Note in Zehnteln ist Math.floor((1020 - total) / 18). Prüf das mit einer Punktzahl aus der Tabelle nach.

Lösung anzeigen
function abiturGrade(total) {  if (total < 300 || total > 900) {    throw new RangeError(`${total} Punkte: Die Gesamtqualifikation liegt zwischen 300 und 900`);  }  // in Zehnteln: (1020 - total) / 18, nach unten abgeschnitten  const tenths = Math.floor((1020 - total) / 18);  return Math.max(tenths, 10) / 10;}

1020 ist 17/3 mal 180, und durch 18 statt 180 zu teilen gibt gleich Zehntel. Mit ganzen Zahlen gibt es nichts zu runden und keine Kommazahl, die eine Winzigkeit danebenliegt.

Aufgabe 7Der Fehler ist weg

Ändere abitur_grade in notenrechner/noten.rb so, dass es nach der ersten Nachkommastelle abschneidet, statt zu runden, bis alle deine Tests bestehen. Die Prüfung ruft abitur_grade mit sieben Punktzahlen aus der Tabelle auf.

Tipp

Runden und Abschneiden unterscheiden sich, wenn die zweite Nachkommastelle 5 oder mehr ist: 1,194 wird gerundet 1,2, abgeschnitten 1,1.

Nächster Tipp

Abschneiden auf eine Nachkommastelle heißt mal 10 rechnen, nach unten runden (floor) und wieder durch 10 teilen. Ruby kann das auch in einem Schritt, floor(1).

Nächster Tipp

Es geht auch ganz ohne Kommazahlen: Die Note in Zehnteln ist (1020 - total) / 18, ganzzahlig geteilt. Prüf das mit einer Punktzahl aus der Tabelle nach.

Aufgabe 7Der Fehler ist weg

Ändere abitur_grade in notenrechner/noten.py so, dass es nach der ersten Nachkommastelle abschneidet, statt zu runden, bis alle deine Tests bestehen. Die Prüfung ruft abitur_grade mit sieben Punktzahlen aus der Tabelle auf.

Tipp

Runden und Abschneiden unterscheiden sich, wenn die zweite Nachkommastelle 5 oder mehr ist: 1,194 wird gerundet 1,2, abgeschnitten 1,1.

Nächster Tipp

Abschneiden auf eine Nachkommastelle heißt mal 10 rechnen, nach unten runden (math.floor) und wieder durch 10 teilen.

Nächster Tipp

Es geht auch ganz ohne Kommazahlen: Die Note in Zehnteln ist (1020 - total) // 18. Prüf das mit einer Punktzahl aus der Tabelle nach.

Aufgabe 7Der Fehler ist weg

Ändere abiturGrade in notenrechner/noten.js so, dass es nach der ersten Nachkommastelle abschneidet, statt zu runden, bis alle deine Tests bestehen. Die Prüfung ruft abiturGrade mit sieben Punktzahlen aus der Tabelle auf.

Tipp

Runden und Abschneiden unterscheiden sich, wenn die zweite Nachkommastelle 5 oder mehr ist: 1,194 wird gerundet 1,2, abgeschnitten 1,1.

Nächster Tipp

Abschneiden auf eine Nachkommastelle heißt mal 10 rechnen, nach unten runden (Math.floor statt Math.round) und wieder durch 10 teilen.

Nächster Tipp

Es geht auch ganz ohne Kommazahlen: Die Note in Zehnteln ist Math.floor((1020 - total) / 18). Prüf das mit einer Punktzahl aus der Tabelle nach.

6. Zuerst der Test

Rot, grün, aufräumen

Bisher gab es den Code schon, und du hast Tests dazugeschrieben. Man kann es auch umgekehrt machen: zuerst den Test für etwas, das es noch nicht gibt, dann den Code. Das heißt testgetriebene Entwicklung und geht in drei Schritten, immer wieder:

  1. Rot: Schreib einen Test, der scheitert, weil der Code fehlt.
  2. Grün: Schreib gerade so viel Code, dass der Test besteht.
  3. Aufräumen: Mach den Code lesbarer, ohne dass sich etwas ändert. Die Tests bleiben grün und sagen dir, ob es stimmt.

Wer so arbeitet, überlegt vor dem Code, was er tun soll, und hat am Ende zu allem einen Test.

Kent Beck 2006: ein Mann mit kurzem blondem Haar in einem hellen, gestreiften Hemd, die Hände vor dem Bauch gefaltet, vor einer Wandtafel.

Kent Beck (geboren 1961), US-amerikanischer Softwareentwickler. Erst den Test, der scheitert, dann den Code, der ihn bestehen lässt: So arbeitest du im Notenrechner, und so heißt es testgetriebene Entwicklung. Beck machte sie bekannt; nach seinen Worten hat er sie in einem älteren Buch über das Programmieren wiederentdeckt.

Mit Erich Gamma brachte er das Testwerkzeug SUnit aus Smalltalk nach Java, als JUnit. Nachbauten mit diesem Bauplan für andere Sprachen nennt man xUnit.

Foto: Improve It, 2006, CC BY-SA 2.0, zugeschnitten (Wikimedia Commons)

Die Zulassung zur Prüfung

Zur Abiturprüfung zugelassen wird nach der Vereinbarung der KMK (Ziffer 9.5.7), wer in der Qualifikationsphase genug Punkte hat. Etwas vereinfacht, wenn jedes Ergebnis eines Halbjahrs einfach zählt:

  • Block I: Der Durchschnitt der Ergebnisse mal 40, gerundet, ist mindestens
    1. 36 Ergebnisse mit je 5 Punkten sind genau 200.
  • Unter 5 Punkten: Höchstens 20 Prozent der Ergebnisse haben weniger als 5 Punkte, ein Defizit.
  • Keine 0: Kein Ergebnis hat 0 Punkte.

Die Methode dafür soll admitted?(results) heißen und für eine Liste von Ergebnissen true oder false liefern. Für solche Werte hat minitest assert und refute: assert admitted?([5] * 36) besteht, wenn admitted? wahr ist, refute bei falsch.

Die Funktion dafür soll is_admitted(results) heißen und für eine Liste von Ergebnissen True oder False liefern. assert is_admitted([5] * 36) besteht, wenn das Ergebnis wahr ist, assert not … bei falsch.

Die Funktion dafür soll isAdmitted(results) heißen und für eine Liste von Ergebnissen true oder false liefern. assert.ok(isAdmitted(…)) besteht, wenn das Ergebnis wahr ist, assert.ok(!isAdmitted(…)) bei falsch.

Jetzt bist du dran, rot: Schreib die Tests für die Zulassung, bevor es den Code gibt: für jede der drei Regeln einen Fall, der gerade noch zugelassen wird, und einen, der gerade nicht mehr zugelassen wird. Lass sie laufen; sie scheitern, weil es die Funktion noch nicht gibt.
Tipp

Bei 40 Ergebnissen sind 20 Prozent genau 8. Damit Block I nicht der Grund ist, brauchen die übrigen genug Punkte: [4] * 8 + [6] * 32.

Lösung anzeigen
# die Zulassung: Block I, Ergebnisse unter 5 Punkten, 0 Punkte
def test_admitted
  assert admitted?([5] * 36)                 # genau 200 Punkte in Block I
  refute admitted?([5] * 35 + [4])           # 199 Punkte
  assert admitted?([4] * 8 + [6] * 32)       # 8 von 40 unter 5 Punkten: genau 20 Prozent
  refute admitted?([4] * 9 + [6] * 31)       # 9 von 40
  assert admitted?([5] * 9 + [6] * 31)       # 5 Punkte liegen nicht unter 5
  refute admitted?([0] + [15] * 35)          # ein Ergebnis mit 0 Punkten
end
Tipp

Bei 40 Ergebnissen sind 20 Prozent genau 8. Damit Block I nicht der Grund ist, brauchen die übrigen genug Punkte: [4] * 8 + [6] * 32. is_admitted kommt in die Zeile from noten import ….

Lösung anzeigen
# die Zulassung: Block I, Ergebnisse unter 5 Punkten, 0 Punkte
def test_admitted():
    assert is_admitted([5] * 36)                     # genau 200 Punkte in Block I
    assert not is_admitted([5] * 35 + [4])           # 199 Punkte
    assert is_admitted([4] * 8 + [6] * 32)           # 8 von 40 unter 5 Punkten: genau 20 Prozent
    assert not is_admitted([4] * 9 + [6] * 31)       # 9 von 40
    assert is_admitted([5] * 9 + [6] * 31)           # 5 Punkte liegen nicht unter 5
    assert not is_admitted([0] + [15] * 35)          # ein Ergebnis mit 0 Punkten
Tipp

Bei 40 Ergebnissen sind 20 Prozent genau 8. Eine kleine Funktion spart Tipparbeit: const results = (points, count) => Array(count).fill(points); macht count Ergebnisse mit je points Punkten.

Lösung anzeigen
// count Ergebnisse mit je points Punkten
const results = (points, count) => Array(count).fill(points);

// die Zulassung: Block I, Ergebnisse unter 5 Punkten, 0 Punkte
test('isAdmitted', () => {
  assert.ok(isAdmitted(results(5, 36)));                                  // genau 200 Punkte in Block I
  assert.ok(!isAdmitted([...results(5, 35), 4]));                         // 199 Punkte
  assert.ok(isAdmitted([...results(4, 8), ...results(6, 32)]));           // 8 von 40 unter 5 Punkten: genau 20 Prozent
  assert.ok(!isAdmitted([...results(4, 9), ...results(6, 31)]));          // 9 von 40
  assert.ok(isAdmitted([...results(5, 9), ...results(6, 31)]));           // 5 Punkte liegen nicht unter 5
  assert.ok(!isAdmitted([0, ...results(15, 35)]));                        // ein Ergebnis mit 0 Punkten
});
Aufgabe 8Zuerst die Tests

Schreib in notenrechner/test_noten.rb Tests für admitted?, bevor es die Methode gibt: für Block I, für die Ergebnisse unter 5 Punkten und für 0 Punkte, jeweils an der Grenze. Mit deinem noten.rb scheitern sie. Die Prüfung führt sie mit einem richtigen noten.rb aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften.

Tipp

Für jede der drei Regeln zwei Tests: einer, der gerade noch zugelassen wird, und einer, der gerade nicht mehr zugelassen wird.

Nächster Tipp

Block I: 36 Ergebnisse mit je 5 Punkten sind im Durchschnitt 5, mal 40 genau 200. Mit einer 4 statt einer 5 reicht es nicht mehr. [5] * 36 schreibt die 36 Ergebnisse kurz.

Nächster Tipp

Die 20 Prozent: Bei 40 Ergebnissen dürfen 8 unter 5 Punkten liegen, 9 nicht. Die übrigen brauchen genug Punkte, damit Block I nicht der Grund ist: [4] * 8 + [6] * 32.

Aufgabe 8Zuerst die Tests

Schreib in notenrechner/test_noten.py Tests für is_admitted, bevor es die Funktion gibt: für Block I, für die Ergebnisse unter 5 Punkten und für 0 Punkte, jeweils an der Grenze. Mit deinem noten.py scheitern sie. Die Prüfung führt sie mit einem richtigen noten.py aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften.

Tipp

Für jede der drei Regeln zwei Tests: einer, der gerade noch zugelassen wird, und einer, der gerade nicht mehr zugelassen wird.

Nächster Tipp

Block I: 36 Ergebnisse mit je 5 Punkten sind im Durchschnitt 5, mal 40 genau 200. Mit einer 4 statt einer 5 reicht es nicht mehr. [5] * 36 schreibt die 36 Ergebnisse kurz.

Nächster Tipp

Die 20 Prozent: Bei 40 Ergebnissen dürfen 8 unter 5 Punkten liegen, 9 nicht. Die übrigen brauchen genug Punkte, damit Block I nicht der Grund ist: [4] * 8 + [6] * 32.

Aufgabe 8Zuerst die Tests

Schreib in notenrechner/test_noten.js Tests für isAdmitted, bevor es die Funktion gibt: für Block I, für die Ergebnisse unter 5 Punkten und für 0 Punkte, jeweils an der Grenze. Mit deinem noten.js scheitern sie. Die Prüfung führt sie mit einem richtigen noten.js aus, mit dem alle bestehen müssen, und mit fünf fehlerhaften.

Tipp

Für jede der drei Regeln zwei Tests: einer, der gerade noch zugelassen wird, und einer, der gerade nicht mehr zugelassen wird.

Nächster Tipp

Block I: 36 Ergebnisse mit je 5 Punkten sind im Durchschnitt 5, mal 40 genau 200. Mit einer 4 statt einer 5 reicht es nicht mehr. Array(36).fill(5) schreibt die 36 Ergebnisse kurz.

Nächster Tipp

Die 20 Prozent: Bei 40 Ergebnissen dürfen 8 unter 5 Punkten liegen, 9 nicht. Die übrigen brauchen genug Punkte, damit Block I nicht der Grund ist: [...Array(8).fill(4), ...Array(32).fill(6)].

Grün

Jetzt bist du dran, grün: Schreib jetzt die Funktion für die Zulassung in noten, bis alle Tests grün sind.
Tipp

Rechne zuerst die drei Werte aus, die die Regeln brauchen, und verknüpf dann die drei Bedingungen mit &&. Block I ist (results.sum * 40.0 / results.size).round; die 20 Prozent gehen ohne Kommazahlen: Anzahl unter 5 mal 5 ist höchstens so groß wie die Anzahl aller.

Lösung anzeigen
def admitted?(results)
  # Block I: der Durchschnitt mal 40, gerundet
  block_one = (results.sum * 40.0 / results.size).round
  deficits = results.count { |points| deficit?(points) }
  block_one >= 200 && deficits * 5 <= results.size && !results.include?(0)
end
Tipp

Rechne zuerst die drei Werte aus, die die Regeln brauchen, und verknüpf dann die drei Bedingungen mit and. Block I ist math.floor(sum(results) * 40 / len(results) + 0.5), denn Pythons round rundet ,5 zur geraden Zahl. Die 20 Prozent gehen ohne Kommazahlen: Anzahl unter 5 mal 5 ist höchstens so groß wie die Anzahl aller.

Lösung anzeigen
def is_admitted(results):
    # Block I: der Durchschnitt mal 40, ab ,5 aufgerundet
    block_one = math.floor(sum(results) * 40 / len(results) + 0.5)
    deficits = len([points for points in results if is_deficit(points)])
    return block_one >= 200 and deficits * 5 <= len(results) and 0 not in results

import math kommt dazu ganz oben in die Datei.

Tipp

Rechne zuerst die drei Werte aus, die die Regeln brauchen, und verknüpf dann die drei Bedingungen mit &&. Block I ist Math.round(sum * 40 / results.length); die 20 Prozent gehen ohne Kommazahlen: Anzahl unter 5 mal 5 ist höchstens so groß wie die Anzahl aller.

Lösung anzeigen
function isAdmitted(results) {
  // Block I: der Durchschnitt mal 40, gerundet
  const sum = results.reduce((total, points) => total + points, 0);
  const blockOne = Math.round(sum * 40 / results.length);
  const deficits = results.filter(isDeficit).length;
  return blockOne >= 200 && deficits * 5 <= results.length && !results.includes(0);
}

In module.exports kommt isAdmitted dazu.

Aufgabe 9Dann der Code

Schreib jetzt admitted?(results) in notenrechner/noten.rb, bis alle deine Tests bestehen. Die Prüfung ruft es mit sechs anderen Listen von Ergebnissen auf als deine Tests.

Tipp

Rechne zuerst die drei Werte aus, die die Regeln brauchen, Block I, wie viele Ergebnisse unter 5 Punkten liegen und ob eine 0 darunter ist, und verknüpf dann die drei Bedingungen.

Nächster Tipp

Block I ist die Summe geteilt durch die Anzahl, mal 40, gerundet: (results.sum * 40.0 / results.size).round. Die 20 Prozent ohne Kommazahlen: Anzahl unter 5 mal 5 höchstens so groß wie die Anzahl aller.

Nächster Tipp

Für die Ergebnisse unter 5 Punkten gibt es deficit? schon in noten.rb, und count mit einem Block zählt, wofür es wahr ist.

Aufgabe 9Dann der Code

Schreib jetzt is_admitted(results) in notenrechner/noten.py, bis alle deine Tests bestehen. Die Prüfung ruft es mit sechs anderen Listen von Ergebnissen auf als deine Tests.

Tipp

Rechne zuerst die drei Werte aus, die die Regeln brauchen, Block I, wie viele Ergebnisse unter 5 Punkten liegen und ob eine 0 darunter ist, und verknüpf dann die drei Bedingungen.

Nächster Tipp

Block I ist die Summe geteilt durch die Anzahl, mal 40, ab ,5 aufgerundet: math.floor(sum(results) * 40 / len(results) + 0.5). Pythons round rundet ,5 zur geraden Zahl. Die 20 Prozent ohne Kommazahlen: Anzahl unter 5 mal 5 höchstens so groß wie die Anzahl aller.

Nächster Tipp

Für die Ergebnisse unter 5 Punkten gibt es is_deficit schon in noten.py; len einer Liste mit nur diesen Ergebnissen zählt sie.

Aufgabe 9Dann der Code

Schreib jetzt isAdmitted(results) in notenrechner/noten.js, bis alle deine Tests bestehen, und nimm es in module.exports auf. Die Prüfung ruft es mit sechs anderen Listen von Ergebnissen auf als deine Tests.

Tipp

Rechne zuerst die drei Werte aus, die die Regeln brauchen, Block I, wie viele Ergebnisse unter 5 Punkten liegen und ob eine 0 darunter ist, und verknüpf dann die drei Bedingungen.

Nächster Tipp

Block I ist die Summe geteilt durch die Anzahl, mal 40, gerundet: Math.round(sum * 40 / results.length). Die 20 Prozent ohne Kommazahlen: Anzahl unter 5 mal 5 höchstens so groß wie die Anzahl aller.

Nächster Tipp

Für die Ergebnisse unter 5 Punkten gibt es isDeficit schon in noten.js, und results.filter(isDeficit).length zählt sie.

Ausprobieren, aufräumen: Schreib die Zulassung anders, etwa mit drei Zeilen, die jede für sich früh zurückgeben, wenn eine Regel verletzt ist. Lass nach jeder Änderung die Tests laufen: Solange alle grün sind, verhält sich der Code wie vorher. Welche Fassung liest sich leichter?

7. Was Tests nicht können

Die Prüfung dieser Seite hat deine Tests selbst getestet: mit Fassungen von noten, in die je ein kleiner Fehler eingebaut war, ein > statt >=, eine 16 statt einer 15. Ein Test, der jeden dieser Fehler findet, ist gut, und so prüft man Tests auch außerhalb der Schule: Das heißt Mutationstest, die fehlerhaften Fassungen heißen Mutanten.

Wie viele Tests auch grün sind, ein Beweis sind sie nicht. Edsger W. Dijkstra schrieb 1969 in seinen »Notes on Structured Programming«: »Program testing can be used to show the presence of bugs, but never to show their absence!« Tests zeigen, dass ein Programm Fehler hat, aber nie, dass es keine hat. Darum testet man dort, wo Fehler wahrscheinlich sind: an den Grenzen.

Edsger W. Dijkstra 2002: ein Mann mit Brille, grauem Bart und roter Pullunderweste, im Halbprofil.

Edsger W. Dijkstra (1930–2002), niederländischer Informatiker. Sein Verfahren für den kürzesten Weg erdachte er 1956 in zwanzig Minuten auf einer Caféterrasse in Amsterdam, ohne Stift und Papier. Navis und der Routenplaner suchen ihre Wege mit seiner Idee.

Dass du Schleifen und Funktionen schreibst, statt mit goto kreuz und quer durchs Programm zu springen, geht auch auf ihn zurück: »Go To Statement Considered Harmful«, 1968.

Für Programme, die nebeneinander laufen, erfand er 1962 oder 1963 den Semaphor, der regelt, wer an eine gemeinsame Sache darf, und 1965 eine Prüfungsaufgabe über Deadlocks: die speisenden Philosophen.

Foto: Hamilton Richards, 2002, CC BY-SA 3.0, zugeschnitten (Wikimedia Commons)

8. Wie geht es weiter?

  • In den Katas: Tests schreiben findest du mehr Funktionen, für die du Tests schreibst, die jeden eingebauten Fehler finden.
  • Im Klassen-Quiz arbeitet ihr zu vielen an einem Programm. Tests sagen dir dort, ob deine Änderung etwas kaputt macht, das jemand anderes geschrieben hat.
  • Für die Funktionen aus den Prüfziffern kannst du Tests nach derselben Idee schreiben: An welchen Grenzen kann eine Prüfziffer falsch sein?