Debugging von Testfehlern

Wenn Tests fehlschlagen, müssen wir herausfinden, warum. Vielleicht liegt es am Test, vielleicht aber auch an der Anwendung. Der Prozess, um herauszufinden, wo das Problem liegt und was man dagegen tun kann, ist ähnlich.

pytest bietet viele Werkzeuge, die uns helfen können, ein Problem schneller zu lösen, ohne dass wir zu einem Debugger greifen müssen. Python enthält einen eingebauten Quellcode-Debugger namens pdb, sowie mehrere Optionen, die das Debuggen mit pdb schnell und einfach machen.

Im Folgenden werden wir einige fehlerhafte Codes mit Hilfe von pytest-Optionen und pdb debuggen und uns dabei die Debugging-Optionen und die Integration von pytest und pdb anzusehen.

Debuggen mit pytest-Optionen

pytest enthält eine ganze Reihe von Kommandozeilen-Optionen, die für die Fehlersuche nützlich sind. Wir werden einige davon verwenden, um unsere Testfehler zu beheben. Optionen für die Auswahl, welche Tests in welcher Reihenfolge ausgeführt werden sollen und wann sie gestoppt werden sollen.

In all diesen Beschreibungen bezieht sich der Begriff Fehler auf eine fehlgeschlagene Assertion oder eine andere nicht abgefangene Exception, die in unserem Quell- oder Testcode, einschließlich der Fixtures, gefunden wurde.

  1. Erneute Ausführung fehlgeschlagener Tests

    Beginnen wir mit der Fehlersuche, indem wir sicherstellen, dass die Tests fehlschlagen, wenn wir sie erneut ausführen. Hierfür verwenden wir --lf, um nur die fehlgeschlagenen Tests erneut auszuführen, und --tb=no, um den Traceback auszublenden. So wissen wir , dass wir den Fehler reproduzieren können.

    1. Nun können wir mit dem Debuggen des ersten Fehlers beginnen und führen hierzu den ersten fehlgeschlagenen Testaus, halten nach dem Fehler an und sehen uns den Traceback an: pytest --lf -x.

    2. Um sicher zu gehen, dass wir das Problem verstehen, können wir den gleichen Test mit -l/--showlocals noch einmal durchführen. Wir brauchen den vollständigen Traceback nicht noch einmal, also können wir ihn mit --tb=short kürzen: pytest --lf -x -l --tb=short.

      -l/--showlocals sind oft sehr hilfreich und manchmal gut genug, um einen Testfehler vollständig zu erkennen.

  2. Fehlersuche mit pdb

    pdb ist Teil der Python Standardbibliothek, so dass wir nichts installieren müssen, um es zu benutzen. Ihr könnt pdb von pytest aus auf verschiedene Weise starten:

    • Fügt einen breakpoint()-Aufruf entweder zum Test- oder zum Anwendungscode hinzu. Wenn ein pytest Lauf auf einen breakpoint()-Funktionsaufruf trifft, wird er dort anhalten und pdb starten.

    • Verwendet die --pdb-Option. Mit --pdb hält pytest an der Stelle des Fehlers an.

    • Verwendet die Kombination der --lf und --trace-Optionen. Mit --trace hält pytest am Anfang eines jeden Tests.

      Nachfolgend sind die üblichen Befehle aufgeführt, die von pdb erkannt werden:

      Optionen

      Beschreibung

      Meta-Befehle

      h(elp)

      gibt eine Liste von Befehlen aus.

      h(elp) COMMAND

      gibt die Hilfe zu einem Befehl aus.

      q(uit)

      beendet pdb.

      Sehen, wo ihr seid

      l(ist)

      listet elf Zeilen um die aktuelle Zeile auf; beim erneuten Aufruf werden die nächsten elf Zeilen aufgelistet.

      l(ist) .

      Das Gleiche wie oben, aber mit einem Punkt. Listet elf Zeilen um die aktuelle Zeile auf. Praktisch, wenn ihr l(list) ein paar Mal benutzt habt und eure aktuelle Position verloren habt.

      l(ist) first|last

      listet eine bestimmte Gruppe von Zeilen auf.

      ll

      listet den gesamten Quellcode für die aktuelle Funktion auf.

      w(here)

      gibt den Stack-Trace aus.

      Werte ansehen

      p(rint) EXPR

      wertet EXPR aus und gibt Wert aus.

      pp EXPR

      entspricht p(rint) EXPR, verwendet aber pretty-print aus dem pprint-Modul.

      a(rgs)

      gibt die Argumentliste der aktuellen Funktion aus.

      Ausführungsbefehle

      s(tep)

      führt die aktuelle Zeile aus und springt zur nächsten Zeile in Ihrem Quellcode, auch wenn sie sich innerhalb einer Funktion befindet.

      n(ext)

      führt die aktuelle Zeile aus und springt zur nächsten Zeile in der aktuellen Funktion.

      c(ontinue)

      wird bis zum nächsten Haltepunkt fortgesetzt. Bei Verwendung mit --trace bis zum Beginn des nächsten Tests fortgesetzt.

      unt(il) LINENO

      wird bis zur angegebenen Zeilennummer fortgesetzt.

      Siehe auch

      Die vollständige Liste findet ihr in Debugger Commands der pdb-Dokumentation.

Kombinieren von pdb und tox

Um pdb mit tox kombinieren zu können, müssen wir sicherstellen, dass wir Argumente durch tox an pytest übergeben können. Dies geschieht mit der {posargs}-Funktion von tox, die in pytest-Parameter an tox übergeben beschrieben wurde. Wir haben diese Funktion bereits in unserer tox.ini für Items eingerichtet:

[tox]
envlist = py38, py39, py310, py311
isolated_build = True
skip_missing_interpreters = True

[testenv]
deps =
  pytest
  faker
  pytest-cov
commands = pytest --cov=items --cov-fail-under=99  {posargs}

[gh-actions]
python =
  3.8: py38
  3.9: py39
  3.10: py310
  3.11: py311

Wir möchten die Python 3.11-Umgebung ausführen und den Debugger bei einem fehlgeschlagenen Test starten mit tox -e py311 -- --pdb --no-cov. Das bringt uns in den pdb, genau an der Assertion, die fehlgeschlagen ist.

Nachdem wir den Fehler gefunden und behoben haben, können wir die Tox-Umgebung mit diesem einen Testfehler erneut ausführen: tox -e py311 -- --lf --tb=no --no-cov.

Überblick über die gebräuchlichsten pytest-Debugger-Optionen

Optionen

Beschreibung

Optionen für die Auswahl, welche Tests in welcher Reihenfolge ausgeführt werden sollen und wann sie gestoppt werden sollen:

--lf, --last-failedlf

führt den zuerst fehlgeschlagenen Test aus

--ff, --failed-first

startet mit dem zuerst fehlgeschlagenen Test und führt dann alle aus.

-x, --exitfirst

hält beim ersten Fehler an und führt dann alle aus.

-maxfail=NUM

stoppt die Tests nach NUM Fehlern.

--nf, --new-first

führt zuerst neue Testdateien aus, dann den Rest sortiert nach Änderungsdatum.

--sw, --stepwise

führt den letzten fehlgeschlagenen Test aus, stoppt dann beim nächsten Fehler und startet beim nächsten Mal wieder beim letzten fehlgeschlagenen Test. Ähnlich wie die Kombination von --lf -x, aber effizienter.

--sw-skip, --stepwise-skip

wie oben, aber ein fehlgeschlagener Test wird übersprungen.

Optionen zur Kontrolle der pytest-Ausgabe:

-v, --verbose:

verbos, -vv ist noch ausführlicher

--tb

Traceback-Stil: [auto|long|short|line|native|no]

Üblicherweise nutze ich --tb=short als Standardeinstellung in der Konfigurationsdatei und die anderen für die Fehlersuche.

-l, --showlocals

zeigt lokale Variablen neben dem Stacktrace an.

Optionen zum Starten eines Kommandozeilen-Debuggers:

--pdb

startet den Python-Debugger im Fehlerfall. Sehr nützlich zum Debuggen mit tox.

--trace

startet den pdb-Quellcode-Debugger sofort bei der Ausführung jedes Tests.

--pdbcls

verwendet Alternativen zu pdb, z.B. den IPython-Debugger mit --pdb-cls = IPython.terminal.debugger:TerminalPdb