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.

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.
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.
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.
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.
Die Datei beginnt mit zwei Zeilen require, dann kommt eine Klasse, die von Minitest::Test erbt, mit einer Methode, deren Name mit test_ beginnt.
In der Methode steht eine Zeile assert_equal '1+', grade(15), zuerst der erwartete Wert, dann der Aufruf.
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.
Die Datei holt sich grade mit from noten import grade, dann kommt eine Funktion, deren Name mit test_ beginnt.
In der Funktion steht eine Zeile assert grade(15) == "1+".
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.
Die Datei holt sich test, assert und grade mit require, dann kommt ein Aufruf test('…', () => { … }).
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.
Angenommen, du änderst in deinem Test die erwartete Note von 1+ zu 1
und lässt ihn noch einmal laufen. Was passiert?
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.



Setz die Erwartung danach wieder auf 1+.
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.
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?
Wo endet das, was es gibt, und wo beginnt das, was es nicht gibt? Eine Grenze hat zwei Seiten.
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.
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.
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?
# 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
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?
# 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)
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?
// 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); });
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.
Drei Arten von Tests: die Enden (0 und 15), etwas dazwischen und das, was es nicht gibt. Schreib für jede Art eine Methode.
Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die für die Oberstufe zählt. Welche Note gehört zu 5 Punkten?
Für die Punkte, die es nicht gibt, steht der Aufruf in einem Block hinter assert_raises(ArgumentError), wie oben bei 'zwölf'.
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.
Drei Arten von Tests: die Enden (0 und 15), etwas dazwischen und das, was es nicht gibt. Schreib für jede Art eine Funktion.
Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die für die Oberstufe zählt. Welche Note gehört zu 5 Punkten?
Für die Punkte, die es nicht gibt, steht der Aufruf in einem Block with pytest.raises(ValueError):, wie oben bei "zwölf".
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.
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.
Bei 5 Punkten liegt mitten im Bereich noch eine Grenze, die für die Oberstufe zählt. Welche Note gehört zu 5 Punkten?
Für die Punkte, die es nicht gibt, steht der Aufruf in einer Funktion in assert.throws(() => …, RangeError), wie oben bei 'zwölf'.
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.
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.
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.
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.
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.
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.
noten hat einen Fehler, und dafür sind sie da.
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).
# 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
823 und 822 liegen auf den beiden Seiten einer Grenze. abitur_grade holst du
dir mit from noten import abitur_grade, grade.
# 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)
823 und 822 liegen auf den beiden Seiten einer Grenze. abiturGrade holst du
dir in derselben Zeile wie grade: const { grade, abiturGrade } = require('./noten');.
// 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.
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.
Such in der Tabelle eine Grenze, an der eine neue Note beginnt. Welche zwei Punktzahlen liegen direkt an ihr?
Die Enden der Tabelle sind 900 und 300 Punkte; gleich daneben liegen 901 und 299, wo es einen Fehler geben muss.
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.
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.
Such in der Tabelle eine Grenze, an der eine neue Note beginnt. Welche zwei Punktzahlen liegen direkt an ihr?
Die Enden der Tabelle sind 900 und 300 Punkte; gleich daneben liegen 901 und 299, wo es einen Fehler geben muss.
assert abitur_grade(823) == 1.0 prüft eine Grenze von der einen Seite, assert abitur_grade(822) == 1.1 von der anderen.
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.
Such in der Tabelle eine Grenze, an der eine neue Note beginnt. Welche zwei Punktzahlen liegen direkt an ihr?
Die Enden der Tabelle sind 900 und 300 Punkte; gleich daneben liegen 901 und 299, wo es einen Fehler geben muss.
assert.strictEqual(abiturGrade(823), 1.0) prüft eine Grenze von der einen Seite, assert.strictEqual(abiturGrade(822), 1.1) von der anderen.
noten so, dass sie nach der ersten Nachkommastelle abschneidet,
statt zu runden. Lass nach jeder Änderung die Tests laufen, bis alle grün sind.
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.
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
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.
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
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.
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.
Ä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.
Runden und Abschneiden unterscheiden sich, wenn die zweite Nachkommastelle 5 oder mehr ist: 1,194 wird gerundet 1,2, abgeschnitten 1,1.
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).
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.
Ä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.
Runden und Abschneiden unterscheiden sich, wenn die zweite Nachkommastelle 5 oder mehr ist: 1,194 wird gerundet 1,2, abgeschnitten 1,1.
Abschneiden auf eine Nachkommastelle heißt mal 10 rechnen, nach unten runden (math.floor) und wieder durch 10 teilen.
Es geht auch ganz ohne Kommazahlen: Die Note in Zehnteln ist (1020 - total) // 18. Prüf das mit einer Punktzahl aus der Tabelle nach.
Ä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.
Runden und Abschneiden unterscheiden sich, wenn die zweite Nachkommastelle 5 oder mehr ist: 1,194 wird gerundet 1,2, abgeschnitten 1,1.
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 ganz ohne Kommazahlen: Die Note in Zehnteln ist Math.floor((1020 - total) / 18). Prüf das mit einer Punktzahl aus der Tabelle nach.
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:
Wer so arbeitet, überlegt vor dem Code, was er tun soll, und hat am Ende zu allem einen Test.
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)
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:
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.
Bei 40 Ergebnissen sind 20 Prozent genau 8. Damit Block I nicht der Grund ist,
brauchen die übrigen genug Punkte: [4] * 8 + [6] * 32.
# 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
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 ….
# 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
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.
// 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 });
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.
Für jede der drei Regeln zwei Tests: einer, der gerade noch zugelassen wird, und einer, der gerade nicht mehr zugelassen wird.
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.
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.
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.
Für jede der drei Regeln zwei Tests: einer, der gerade noch zugelassen wird, und einer, der gerade nicht mehr zugelassen wird.
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.
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.
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.
Für jede der drei Regeln zwei Tests: einer, der gerade noch zugelassen wird, und einer, der gerade nicht mehr zugelassen wird.
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.
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)].
noten, bis alle Tests grün sind.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Für die Ergebnisse unter 5 Punkten gibt es isDeficit schon in noten.js, und results.filter(isDeficit).length zählt sie.
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 (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)