Sie verwenden pytest
, wodurch Sie zahlreiche Optionen für die Interaktion mit fehlgeschlagenen Tests erhalten. Es gibt Ihnen Befehlszeilenoptionen und mehrere Hooks, um dies zu ermöglichen. Ich werde erklären, wie Sie die einzelnen Funktionen verwenden und wo Sie Anpassungen vornehmen können, die Ihren spezifischen Debugging-Anforderungen entsprechen.
Ich werde auch auf exotischere Optionen eingehen, mit denen Sie bestimmte Behauptungen vollständig überspringen können, wenn Sie wirklich das Gefühl haben, dass Sie es müssen.
Ausnahmen behandeln, nicht behaupten
Beachten Sie, dass ein fehlgeschlagener Test den Pytest normalerweise nicht stoppt. Nur wenn Sie die explizite Anweisung aktiviert haben, nach einer bestimmten Anzahl von Fehlern zu beenden . Außerdem schlagen Tests fehl, weil eine Ausnahme ausgelöst wird. assert
erhöht, AssertionError
aber das ist nicht die einzige Ausnahme, die dazu führt, dass ein Test fehlschlägt! Sie möchten steuern, wie Ausnahmen behandelt und nicht geändert werden assert
.
Eine fehlgeschlagene Zusicherung beendet jedoch den einzelnen Test. Das liegt daran try...except
, dass Python, sobald eine Ausnahme außerhalb eines Blocks ausgelöst wird, den aktuellen Funktionsrahmen abwickelt und es kein Zurück mehr gibt.
Ich glaube nicht, dass Sie das wollen, gemessen an Ihrer Beschreibung Ihrer _assertCustom()
Versuche, die Behauptung erneut auszuführen, aber ich werde Ihre Optionen dennoch weiter unten erörtern.
Post-Mortem-Debugging im Pytest mit pdb
Für die verschiedenen Optionen zur Behandlung von Fehlern in einem Debugger beginne ich mit dem --pdb
Befehlszeilenschalter , der die Standard-Debugging-Eingabeaufforderung öffnet, wenn ein Test fehlschlägt (die Ausgabe wurde der Kürze halber entfernt):
$ mkdir demo
$ touch demo/__init__.py
$ cat << EOF > demo/test_foo.py
> def test_ham():
> assert 42 == 17
> def test_spam():
> int("Vikings")
> EOF
$ pytest demo/test_foo.py --pdb
[ ... ]
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(2)test_ham()
-> assert 42 == 17
(Pdb) q
Exit: Quitting debugger
[ ... ]
Wenn ein Test fehlschlägt, startet pytest mit diesem Schalter eine Post-Mortem- Debugging-Sitzung . Dies ist im Wesentlichen genau das, was Sie wollten; um den Code an der Stelle eines fehlgeschlagenen Tests zu stoppen und den Debugger zu öffnen, um den Status Ihres Tests zu überprüfen. Sie können mit den lokalen Variablen des Tests, den Globals sowie den Lokalen und Globals jedes Frames im Stapel interagieren.
Hier haben Sie mit pytest die volle Kontrolle darüber, ob Sie nach diesem Punkt q
beenden möchten oder nicht: Wenn Sie den Befehl quit verwenden, beendet pytest den Lauf ebenfalls. Wenn Sie c
for continue verwenden, wird die Steuerung an pytest zurückgegeben und der nächste Test ausgeführt.
Verwenden eines alternativen Debuggers
Sie sind dafür nicht an den pdb
Debugger gebunden . Sie können mit dem --pdbcls
Switch einen anderen Debugger einstellen . Jede pdb.Pdb()
kompatible Implementierung würde funktionieren, einschließlich der IPython-Debugger-Implementierung oder der meisten anderen Python-Debugger (für den Pudb-Debugger muss der -s
Switch verwendet werden, oder ein spezielles Plugin ). Der Switch benötigt ein Modul und eine Klasse, z. B. um pudb
Folgendes zu verwenden:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Sie können diese Funktion benutzen , um Ihre eigenen Wrapper - Klasse zu schreiben , um Pdb
das einfach sofort zurück , wenn der spezifische Fehler nicht etwas , das Sie interessiert sind ist. pytest
Anwendungen Pdb()
genau wie pdb.post_mortem()
tut :
p = Pdb()
p.reset()
p.interaction(None, t)
Hier t
ist ein Traceback-Objekt . Wenn p.interaction(None, t)
zurückgegeben wird, pytest
wird mit dem nächsten Test fortgefahren , sofern nicht p.quitting
gesetzt True
(an diesem Punkt wird der Pytest dann beendet).
Hier ist eine Beispielimplementierung, die ausgibt, dass wir das Debuggen ablehnen, und sofort zurückgibt, sofern der Test nicht ausgelöst wurde ValueError
, gespeichert als demo/custom_pdb.py
:
import pdb, sys
class CustomPdb(pdb.Pdb):
def interaction(self, frame, traceback):
if sys.last_type is not None and not issubclass(sys.last_type, ValueError):
print("Sorry, not interested in this failure")
return
return super().interaction(frame, traceback)
Wenn ich dies mit der obigen Demo verwende, wird dies ausgegeben (der Kürze halber wieder entfernt):
$ pytest test_foo.py -s --pdb --pdbcls=demo.custom_pdb:CustomPdb
[ ... ]
def test_ham():
> assert 42 == 17
E assert 42 == 17
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sorry, not interested in this failure
F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
Die obigen Introspektionen, sys.last_type
um festzustellen, ob der Fehler "interessant" ist.
Ich kann diese Option jedoch nur empfehlen, wenn Sie Ihren eigenen Debugger mit tkInter oder ähnlichem schreiben möchten. Beachten Sie, dass dies ein großes Unterfangen ist.
Filterfehler; Wählen Sie aus, wann der Debugger geöffnet werden soll
Die nächste Stufe sind die Pytest- Debugging- und Interaktions- Hooks . Dies sind Hook-Points für Verhaltensanpassungen, um zu ersetzen oder zu verbessern, wie Pytest normalerweise Dinge wie die Behandlung einer Ausnahme oder die Eingabe des Debuggers über pdb.set_trace()
oder breakpoint()
(Python 3.7 oder neuer) behandelt.
Die interne Implementierung dieses Hooks ist auch für das Drucken des >>> entering PDB >>>
obigen Banners verantwortlich. Wenn Sie diesen Hook verwenden, um zu verhindern, dass der Debugger ausgeführt wird, wird diese Ausgabe überhaupt nicht angezeigt. Sie können Ihren eigenen Hook haben und dann an den ursprünglichen Hook delegieren, wenn ein Testfehler "interessant" ist, und so Testfehler unabhängig vom verwendeten Debugger filtern ! Sie können auf die interne Implementierung zugreifen, indem Sie über den Namen darauf zugreifen . Das interne Hook-Plugin dafür heißt pdbinvoke
. Um zu verhindern, dass es ausgeführt wird, müssen Sie die Registrierung aufheben , aber eine Referenz speichern. Können wir es bei Bedarf direkt aufrufen?
Hier ist eine Beispielimplementierung eines solchen Hooks; Sie können dies an einer beliebigen Stelle ablegen, von der Plugins geladen werden . Ich habe es eingefügt demo/conftest.py
:
import pytest
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
# unregister returns the unregistered plugin
pdbinvoke = config.pluginmanager.unregister(name="pdbinvoke")
if pdbinvoke is None:
# no --pdb switch used, no debugging requested
return
# get the terminalreporter too, to write to the console
tr = config.pluginmanager.getplugin("terminalreporter")
# create or own plugin
plugin = ExceptionFilter(pdbinvoke, tr)
# register our plugin, pytest will then start calling our plugin hooks
config.pluginmanager.register(plugin, "exception_filter")
class ExceptionFilter:
def __init__(self, pdbinvoke, terminalreporter):
# provide the same functionality as pdbinvoke
self.pytest_internalerror = pdbinvoke.pytest_internalerror
self.orig_exception_interact = pdbinvoke.pytest_exception_interact
self.tr = terminalreporter
def pytest_exception_interact(self, node, call, report):
if not call.excinfo. errisinstance(ValueError):
self.tr.write_line("Sorry, not interested!")
return
return self.orig_exception_interact(node, call, report)
Das obige Plugin verwendet das interne TerminalReporter
Plugin , um Zeilen an das Terminal zu schreiben. Dies macht die Ausgabe sauberer, wenn das standardmäßige kompakte Teststatusformat verwendet wird, und ermöglicht es Ihnen, Dinge auf das Terminal zu schreiben, auch wenn die Ausgabeerfassung aktiviert ist.
In diesem Beispiel wird das Plugin-Objekt mit dem pytest_exception_interact
Hook über einen anderen Hook registriert. Stellen Sie pytest_configure()
jedoch sicher, dass es spät genug ausgeführt wird (mithilfe @pytest.hookimpl(trylast=True)
), um die Registrierung des internen pdbinvoke
Plugins aufheben zu können. Wenn der Hook aufgerufen wird, testet das Beispiel das call.exceptinfo
Objekt . Sie können auch den Knoten oder den Bericht überprüfen .
Wenn der obige Beispielcode vorhanden ist demo/conftest.py
, wird der test_ham
Testfehler ignoriert. Nur der test_spam
Testfehler, der ausgelöst wird ValueError
, führt zum Öffnen der Debug-Eingabeaufforderung:
$ pytest demo/test_foo.py --pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
Um es noch einmal zu wiederholen, hat der obige Ansatz den zusätzlichen Vorteil, dass Sie dies mit jedem Debugger kombinieren können, der mit pytest funktioniert , einschließlich pudb oder dem IPython-Debugger:
$ pytest demo/test_foo.py --pdb --pdbcls=IPython.core.debugger:Pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
1 def test_ham():
2 assert 42 == 17
3 def test_spam():
----> 4 int("Vikings")
ipdb>
Es hat auch viel mehr Kontext darüber, welcher Test ausgeführt wurde (über das node
Argument) und direkten Zugriff auf die ausgelöste Ausnahme (über die call.excinfo
ExceptionInfo
Instanz).
Beachten Sie, dass bestimmte Pytest-Debugger-Plugins (wie pytest-pudb
oder pytest-pycharm
) ihre eigenen pytest_exception_interact
Hooksp registrieren . Eine vollständigere Implementierung müsste alle Plugins im Plugin-Manager durchlaufen , um beliebige Plugins automatisch zu überschreiben config.pluginmanager.list_name_plugin
und hasattr()
jedes Plugin zu verwenden und zu testen.
Fehler verschwinden lassen
Auf diese Weise haben Sie zwar die volle Kontrolle über das Debuggen fehlgeschlagener Tests, der Test bleibt jedoch weiterhin fehlgeschlagen, selbst wenn Sie den Debugger für einen bestimmten Test nicht geöffnet haben. Wenn Sie Fehler ganz beseitigen möchten, können Sie einen anderen Haken verwenden : pytest_runtest_call()
.
Wenn pytest Tests ausführt, wird der Test über den obigen Hook ausgeführt, von dem erwartet wird, dass er None
eine Ausnahme zurückgibt oder auslöst . Daraus wird ein Bericht erstellt, optional ein Protokolleintrag erstellt, und wenn der Test fehlgeschlagen ist, wird der oben genannte pytest_exception_interact()
Hook aufgerufen. Alles, was Sie tun müssen, ist zu ändern, was das Ergebnis dieses Hakens erzeugt. Anstelle einer Ausnahme sollte es überhaupt nichts zurückgeben.
Der beste Weg, dies zu tun, ist die Verwendung eines Hook-Wrappers . Hook Wrapper müssen nicht die eigentliche Arbeit erledigen, sondern haben die Möglichkeit zu ändern, was mit dem Ergebnis eines Hooks passiert. Alles was Sie tun müssen, ist die Zeile hinzuzufügen:
outcome = yield
In Ihrer Hook-Wrapper-Implementierung erhalten Sie Zugriff auf das Hook-Ergebnis , einschließlich der Testausnahme über outcome.excinfo
. Dieses Attribut wird auf ein Tupel von (Typ, Instanz, Traceback) gesetzt, wenn im Test eine Ausnahme ausgelöst wurde. Alternativ können Sie die outcome.get_result()
Standardbehandlung aufrufen und verwenden try...except
.
Wie schaffen Sie einen fehlgeschlagenen Test? Sie haben 3 grundlegende Optionen:
- Sie können den Test als erwarteten Fehler markieren , indem Sie
pytest.xfail()
den Wrapper aufrufen .
- Sie können das Element durch Aufrufen als übersprungen markieren , was vorgibt, dass der Test überhaupt nicht ausgeführt wurde
pytest.skip()
.
- Sie können die Ausnahme mithilfe der
outcome.force_result()
Methode entfernen . Setzen Sie das Ergebnis hier auf eine leere Liste (was bedeutet: Der registrierte Hook hat nur etwas produziert None
), und die Ausnahme wird vollständig gelöscht.
Was Sie verwenden, liegt bei Ihnen. Stellen Sie sicher, dass Sie das Ergebnis zuerst auf übersprungene und erwartete Fehlertests überprüfen, da Sie diese Fälle nicht so behandeln müssen, als ob der Test fehlgeschlagen wäre. Sie können auf die speziellen Ausnahmen zugreifen, die diese Optionen über pytest.skip.Exception
und auslösen pytest.xfail.Exception
.
Hier ist eine Beispielimplementierung, die fehlgeschlagene Tests, die nicht ausgelöst werden ValueError
, als übersprungen markiert :
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
outcome = yield
try:
outcome.get_result()
except (pytest.xfail.Exception, pytest.skip.Exception, pytest.exit.Exception):
raise # already xfailed, skipped or explicit exit
except ValueError:
raise # not ignoring
except (pytest.fail.Exception, Exception):
# turn everything else into a skip
pytest.skip("[NOTRUN] ignoring everything but ValueError")
Beim Einfügen wird conftest.py
die Ausgabe:
$ pytest -r a demo/test_foo.py
============================= test session starts =============================
platform darwin -- Python 3.8.0, pytest-3.10.0, py-1.7.0, pluggy-0.8.0
rootdir: ..., inifile:
collected 2 items
demo/test_foo.py sF [100%]
=================================== FAILURES ===================================
__________________________________ test_spam ___________________________________
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
=========================== short test summary info ============================
FAIL demo/test_foo.py::test_spam
SKIP [1] .../demo/conftest.py:12: [NOTRUN] ignoring everything but ValueError
===================== 1 failed, 1 skipped in 0.07 seconds ======================
Ich habe die -r a
Flagge verwendet, um klarer zu machen, dass sie test_ham
jetzt übersprungen wurde.
Wenn Sie den pytest.skip()
Anruf durch ersetzen pytest.xfail("[XFAIL] ignoring everything but ValueError")
, wird der Test als erwarteter Fehler markiert:
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
und mit outcome.force_result([])
markiert es als bestanden:
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
Es liegt an Ihnen, welches Ihrer Meinung nach am besten zu Ihrem Anwendungsfall passt. For skip()
und xfail()
ich haben das Standardnachrichtenformat (mit [NOTRUN]
oder vorangestellt [XFAIL]
) nachgeahmt, aber Sie können jedes andere gewünschte Nachrichtenformat verwenden.
In allen drei Fällen öffnet pytest den Debugger nicht für Tests, deren Ergebnis Sie mit dieser Methode geändert haben.
Ändern einzelner Assert-Anweisungen
Wenn Sie assert
Tests innerhalb eines Tests ändern möchten, bereiten Sie sich auf viel mehr Arbeit vor. Ja, dies ist technisch möglich, aber nur durch Umschreiben des Codes, den Python zur Kompilierungszeit ausführen wird .
Wenn Sie verwenden pytest
, wird dies tatsächlich bereits durchgeführt . Pytest schreibt assert
Anweisungen neu, um Ihnen mehr Kontext zu geben, wenn Ihre Behauptungen fehlschlagen . In diesem Blogbeitrag finden Sie einen guten Überblick über die aktuellen Aktivitäten sowie den _pytest/assertion/rewrite.py
Quellcode . Beachten Sie, dass dieses Modul mehr als 1.000 Zeilen lang ist und dass Sie verstehen müssen, wie die abstrakten Syntaxbäume von Python funktionieren. Wenn Sie das tun, Sie könnten das Modul monkeypatch eigene Modifikationen dort hinzufügen, einschließlich der Umgebung assert
mit einem try...except AssertionError:
Handler.
Allerdings können Sie nicht nur deaktivieren oder ignorieren behauptet selektiv, da nachfolgende Aussagen leicht auf Zustand verlassen können (spezifische Objektarrangements, Variablen gesetzt, etc.) , dass ein übersprungener assert Schutz gegen gemeint war. Wenn ein Assert nicht testet, dann foo
hängt None
ein späterer Assert davon ab foo.bar
, dass er existiert. Dann werden Sie einfach auf einen AttributeError
dort usw. stoßen. Halten Sie sich daran, die Ausnahme erneut auszulösen, wenn Sie diesen Weg gehen müssen.
Ich werde hier nicht näher auf das Umschreiben eingehen asserts
, da ich nicht der Meinung bin, dass es sich lohnt, dies zu verfolgen, da der Arbeitsaufwand nicht gegeben ist und das Post-Mortem-Debugging Ihnen Zugriff auf den Teststatus am Punkt der Behauptung Fehler sowieso .
Beachten Sie, dass Sie in diesem Fall eval()
weder assert
eine Anweisung verwenden müssen (was ohnehin nicht funktionieren würde, ist eine Anweisung, die Sie exec()
stattdessen verwenden müssten), noch die Assertion zweimal ausführen müssten (welche kann zu Problemen führen, wenn der in der Behauptung verwendete Ausdruck den Zustand ändert). Sie würden stattdessen den ast.Assert
Knoten in einen ast.Try
Knoten einbetten und einen Ausnahme-Handler anhängen, der einen leeren ast.Raise
Knoten verwendet, um die abgefangene Ausnahme erneut auszulösen.
Verwenden des Debuggers zum Überspringen von Assertionsanweisungen.
Mit dem Python-Debugger können Sie Anweisungen mit dem Befehl j
/ jump
überspringen . Wenn Sie wissen , vorne , dass eine bestimmte Behauptung wird fehlschlagen, können Sie dies in dem Bypass verwenden. Sie können Ihre Tests mit ausführen --trace
, wodurch der Debugger zu Beginn jedes Tests geöffnet wird , und dann ein ausgeben j <line after assert>
, um ihn zu überspringen, wenn der Debugger kurz vor dem Assert angehalten wird.
Sie können dies sogar automatisieren. Mit den oben genannten Techniken können Sie ein benutzerdefiniertes Debugger-Plugin erstellen, das
- verwendet den
pytest_testrun_call()
Hook, um die AssertionError
Ausnahme abzufangen
- extrahiert die Zeilennummer "beleidigend" aus dem Traceback und ermittelt möglicherweise mit einer Quellcode-Analyse die Zeilennummern vor und nach der Bestätigung, die für die Ausführung eines erfolgreichen Sprungs erforderlich ist
- führt den Test erneut aus , diesmal jedoch unter Verwendung einer
Pdb
Unterklasse, die einen Haltepunkt in der Zeile vor dem Assert festlegt und automatisch einen Sprung zur Sekunde ausführt, wenn der Haltepunkt erreicht wird, gefolgt von einem c
Fortfahren.
Anstatt darauf zu warten, dass eine Zusicherung fehlschlägt, können Sie das Festlegen von Haltepunkten für jeden assert
in einem Test gefundenen Test automatisieren (mithilfe der Quellcode-Analyse können Sie trivialerweise Zeilennummern für ast.Assert
Knoten in einem AST des Tests extrahieren ) und den bestätigten Test ausführen Verwenden Sie Debugger-Skriptbefehle und verwenden Sie den jump
Befehl, um die Zusicherung selbst zu überspringen. Sie müssten einen Kompromiss eingehen; Führen Sie alle Tests unter einem Debugger aus (was langsam ist, da der Interpreter für jede Anweisung eine Trace-Funktion aufrufen muss) oder wenden Sie diese nur auf fehlgeschlagene Tests an und zahlen Sie den Preis für die erneute Ausführung dieser Tests von Grund auf.
Ein solches Plugin wäre eine Menge Arbeit, ich werde hier kein Beispiel schreiben, teils weil es sowieso nicht in eine Antwort passt, teils weil ich nicht denke, dass es die Zeit wert ist . Ich würde einfach den Debugger öffnen und den Sprung manuell machen. Eine fehlgeschlagene Zusicherung weist auf einen Fehler im Test selbst oder im zu testenden Code hin. Sie können sich also genauso gut auf das Debuggen des Problems konzentrieren.