mitmario.dev

Viele Fälle auf einmal

Python Sandbox 3 Min Lesezeit 4 BeispieleLektion 3 von 6

porto() hat drei Grenzen, bei 2000, 10000 und 31500 Gramm. An jede gehören zwei Tests, einer für den letzten Wert, der noch dazugehört, und einer für den ersten, der nicht mehr dazugehört. Wer das ernst nimmt, schreibt also eine Menge Tests, die sich sehr ähnlich sehen.

Sechs Tests, fast gleich

Sechs Tests, fast gleich
from porto import porto


def test_ein_gramm():
    assert porto(1) == 3.99


def test_paeckchen_bis_2000_gramm():
    assert porto(2000) == 3.99


def test_paket_ab_2001_gramm():
    assert porto(2001) == 5.99


def test_paket_bis_10000_gramm():
    assert porto(10000) == 5.99


def test_grosses_paket_ab_10001_gramm():
    assert porto(10001) == 9.99


def test_grosses_paket_bis_31500_gramm():
    assert porto(31500) == 9.99

Die sechs Funktionen unterscheiden sich nur im Namen und in zwei Zahlen. Das kennst du aus Lektion 8.1, nur diesmal bei Tests. Beim Kopieren vergisst man irgendwann, eine der beiden Zahlen anzupassen, und dann prüft ein Test etwas anderes, als sein Name sagt. Und jede weitere Grenze kostet zwei Funktionen mehr.

Eine Tabelle mit parametrize

Eine Tabelle mit parametrize
import pytest

from porto import porto


@pytest.mark.parametrize("gramm, preis", [
    (1, 3.99),
    (2000, 3.99),
    (2001, 5.99),
    (10000, 5.99),
    (10001, 9.99),
    (31500, 9.99),
])
def test_porto(gramm, preis):
    assert porto(gramm) == preis

Derselbe Test, nur einmal geschrieben. Über der Funktion steht @pytest.mark.parametrize(...). Das @ kennst du aus Lektion 14.4. Es ist ein Dekorator, und dieser hängt der Funktion eine Tabelle von Fällen an.

In seinen Klammern stehen zwei Dinge. Zuerst ein Text mit den Namen der Werte, durch Komma getrennt, also "gramm, preis". Danach eine Liste mit einem Tupel je Fall, etwa (2000, 3.99). Die Funktion nimmt genau diese Namen als Parameter.

pytest ruft sie für jedes Tupel einmal auf, beim zweiten mit gramm gleich 2000 und preis gleich 3.99, und zählt jeden Aufruf als eigenen Test. Unten steht deshalb wieder 6 passed.

Mit python -m pytest -v aus der Befehlsleiste läuft pytest mit -v (verbose, ausführlich). Dann steht jeder Fall in einer eigenen Zeile, mit den Werten in eckigen Klammern hinter dem Namen: test_porto[2000-3.99] PASSED. Ein neuer Fall ist jetzt nur noch eine neue Zeile in der Tabelle. Und die Tabelle liest sich wie die Preisliste, die sie prüft.

Welche Fälle in die Tabelle gehören

Die sechs Zeilen sind nicht zufällig gewählt. 2000 Gramm sind das schwerste Päckchen, 2001 Gramm das leichteste Paket, und mit 10000 und 10001 ist es dasselbe eine Stufe höher. 31500 Gramm sind das schwerste große Paket. Das erste Gewicht dahinter kostet gar nichts mehr, sondern löst einen ValueError aus. Tests dafür kommen später in diesem Abschnitt. Dazu kommt noch 1 Gramm, das leichteste Päckchen überhaupt.

Zwei Werte je Grenze braucht es, weil eine Grenze in zwei Richtungen falsch liegen kann. Liegt sie zu tief, etwa mit < statt <=, ist 2000 Gramm schon ein Paket, und das fällt nur beim Fall mit 2000 auf. Liegt sie zu hoch, etwa bei 2100 Gramm, ist 2001 Gramm noch ein Päckchen, und das fällt nur beim Fall mit 2001 auf. Bei einem Wert irgendwo in der Mitte, etwa 1500 Gramm, fällt keiner der beiden Fehler auf.

Ein Fall wird rot

Ein Fall wird rot
import pytest

from porto import porto


@pytest.mark.parametrize("gramm, preis", [
    (1, 3.99),
    (2000, 3.99),
    (2001, 5.99),
    (10000, 5.99),
    (10001, 9.99),
    (31500, 9.99),
])
def test_porto(gramm, preis):
    assert porto(gramm) == preis

Diesmal steckt der Fehler an der zweiten Grenze, gramm < 10000. In der ersten Zeile steht ...F.., und der rote Fall heißt test_porto[10000-5.99]. Darunter stehen die Werte, mit denen er lief, gramm = 10000, preis = 5.99, und dann wie gewohnt der Vergleich. Die anderen fünf Fälle sind gelaufen und grün.

Man könnte auch eine Schleife in einen einzigen Test schreiben, mit for gramm, preis in ...: und einem assert darin. Dann gäbe es aber nur einen Test, und beim ersten roten Fall wäre für alle danach Schluss.

Die Namen passen nicht

Die Namen passen nicht
import pytest

from porto import porto


@pytest.mark.parametrize("gewicht, preis", [
    (1, 3.99),
    (2000, 3.99),
    (2001, 5.99),
    (10000, 5.99),
    (10001, 9.99),
    (31500, 9.99),
])
def test_porto(gramm, preis):
    assert porto(gramm) == preis

Der Fehler dieser Lektion steckt im Text. Dort steht "gewicht, preis", die Funktion heißt aber test_porto(gramm, preis). Dann läuft gar kein Test. pytest meldet einen Fehler beim Einsammeln, ERROR collecting test_porto.py, mit dem Satz In test_porto.py::test_porto: function uses no argument 'gewicht'. Übersetzt heißt das, die Funktion benutzt kein Argument namens gewicht.

Die Namen im Text und die Parameter der Funktion müssen genau übereinstimmen. Buchstabe für Buchstabe.

Was die Aufgabe will

Der Notenschlüssel der IHK aus Abschnitt 3 steht als fertige Funktion note() da. Du schreibst einen Test mit parametrize, der jede Grenze von beiden Seiten prüft. Danach verschiebt pruefen.py jede der fünf Grenzen einmal um einen Punkt nach oben und einmal nach unten, und jedes Mal muss ein Fall deiner Tabelle rot werden.

Zum Mitnehmen

Mit @pytest.mark.parametrize wird aus einer Testfunktion eine ganze Reihe von Tests. Dazu gehören ein Text mit den Namen der Werte und eine Liste mit einem Tupel je Fall, und die Namen müssen genau zu den Parametern passen. pytest meldet jeden Fall einzeln, und an jeder Grenze stehen zwei Fälle, einer auf jeder Seite.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.