Ist es möglich, das Assert-Anweisungsverhalten von PyTest in Python zu ändern?

18

Ich verwende Python-Assert-Anweisungen, um dem tatsächlichen und erwarteten Verhalten zu entsprechen. Ich habe keine Kontrolle darüber, als ob es einen Fehlertest gibt, der abbricht. Ich möchte die Kontrolle über den Assertionsfehler übernehmen und definieren, ob ich den Testfall bei Assertion des Fehlers abbrechen möchte oder nicht.

Außerdem möchte ich etwas hinzufügen, wenn ein Assertionsfehler vorliegt, sollte der Testfall angehalten werden und der Benutzer kann jederzeit fortfahren.

Ich habe keine Ahnung, wie das geht

Codebeispiel, wir verwenden hier pytest

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Below is my expectation

Wenn assert einen assertionError auslöst, sollte ich die Option haben, den Testfall anzuhalten und kann debuggen und später fortsetzen. Für Pause und Lebenslauf werde ich tkinterModul verwenden. Ich werde eine Assert-Funktion wie unten machen

import tkinter
import tkinter.messagebox

top = tkinter.Tk()

def _assertCustom(assert_statement, pause_on_fail = 0):
    #assert_statement will be something like: assert a == 10, "Some error"
    #pause_on_fail will be derived from global file where I can change it on runtime
    if pause_on_fail == 1:
        try:
            eval(assert_statement)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            eval (assert_statement)
            #Above is to raise the assertion error again to fail the testcase
    else:
        eval (assert_statement)

In Zukunft muss ich jede Assert-Anweisung mit dieser Funktion als ändern

import pytest
def test_abc():
    a = 10
    # Suppose some code and below is the assert statement 
    _assertCustom("assert a == 10, 'error message'")

Dies ist zu viel Aufwand für mich, da ich an Tausenden von Stellen, an denen ich behauptet habe, Änderungen vornehmen muss. Gibt es eine einfache Möglichkeit, dies zu tun?pytest

Summary:Ich benötige etwas, bei dem ich den Testfall bei einem Fehler anhalten und nach dem Debuggen fortsetzen kann. Ich weiß davon tkinterund das ist der Grund, warum ich es benutzt habe. Alle anderen Ideen sind willkommen

Note: Der obige Code ist noch nicht getestet. Es können auch kleine Syntaxfehler auftreten

Edit: Danke für die Antworten. Erweitern Sie diese Frage jetzt ein wenig weiter. Was ist, wenn ich das Verhalten von assert ändern möchte? Derzeit wird der Testfall beendet, wenn ein Assertionsfehler vorliegt. Was ist, wenn ich auswählen möchte, ob ich einen Testfall-Exit bei einem bestimmten Assert-Fehler benötige oder nicht? Ich möchte keine benutzerdefinierte Assert-Funktion wie oben erwähnt schreiben, da ich auf diese Weise die Anzahl der Stellen ändern muss

Nitesh
quelle
3
Können Sie uns ein Codebeispiel geben, was Sie tun möchten?
Mrblewog
1
Verwenden Sie keine asserteigenen Überprüfungsfunktionen, sondern schreiben Sie diese, die das tun, was Sie wollen.
Molbdnilo
Warum fügen Sie nicht assert in try block und die Fehlermeldung in excl ein ?
Prathik Kini
1
Es klingt so, als ob Sie es wirklich pytestfür Ihre Testfälle verwenden möchten . Es unterstützt die Verwendung von Assert- und Skipping-Tests sowie viele weitere Funktionen, die das Schreiben von Testsuiten erleichtern.
Blubberdiblub
1
Wäre es nicht ziemlich einfach, ein einfaches Tool zu schreiben, das jeden assert cond, "msg"in Ihrem Code mechanisch ersetzt _assertCustom("assert cond, 'msg'")? Wahrscheinlich sedkönnte es ein Einzeiler tun.
NPE

Antworten:

23

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. asserterhöht, AssertionErroraber 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 --pdbBefehlszeilenschalter , 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 qbeenden möchten oder nicht: Wenn Sie den Befehl quit verwenden, beendet pytest den Lauf ebenfalls. Wenn Sie cfor 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 pdbDebugger gebunden . Sie können mit dem --pdbclsSwitch 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 -sSwitch verwendet werden, oder ein spezielles Plugin ). Der Switch benötigt ein Modul und eine Klasse, z. B. um pudbFolgendes zu verwenden:

$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger

Sie können diese Funktion benutzen , um Ihre eigenen Wrapper - Klasse zu schreiben , um Pdbdas einfach sofort zurück , wenn der spezifische Fehler nicht etwas , das Sie interessiert sind ist. pytestAnwendungen Pdb()genau wie pdb.post_mortem()tut :

p = Pdb()
p.reset()
p.interaction(None, t)

Hier tist ein Traceback-Objekt . Wenn p.interaction(None, t)zurückgegeben wird, pytestwird 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_typeum 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 TerminalReporterPlugin , 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_interactHook ü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 pdbinvokePlugins aufheben zu können. Wenn der Hook aufgerufen wird, testet das Beispiel das call.exceptinfoObjekt . Sie können auch den Knoten oder den Bericht überprüfen .

Wenn der obige Beispielcode vorhanden ist demo/conftest.py, wird der test_hamTestfehler ignoriert. Nur der test_spamTestfehler, 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 nodeArgument) und direkten Zugriff auf die ausgelöste Ausnahme (über die call.excinfo ExceptionInfoInstanz).

Beachten Sie, dass bestimmte Pytest-Debugger-Plugins (wie pytest-pudboder pytest-pycharm) ihre eigenen pytest_exception_interactHooksp registrieren . Eine vollständigere Implementierung müsste alle Plugins im Plugin-Manager durchlaufen , um beliebige Plugins automatisch zu überschreiben config.pluginmanager.list_name_pluginund 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 Noneeine 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.Exceptionund 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.pydie 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 aFlagge verwendet, um klarer zu machen, dass sie test_hamjetzt ü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 assertTests 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 assertAnweisungen 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.pyQuellcode . 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 assertmit 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 foohängt Noneein späterer Assert davon ab foo.bar, dass er existiert. Dann werden Sie einfach auf einen AttributeErrordort 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 asserteine 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.AssertKnoten in einen ast.TryKnoten einbetten und einen Ausnahme-Handler anhängen, der einen leeren ast.RaiseKnoten 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 AssertionErrorAusnahme 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 PdbUnterklasse, 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 cFortfahren.

Anstatt darauf zu warten, dass eine Zusicherung fehlschlägt, können Sie das Festlegen von Haltepunkten für jeden assertin einem Test gefundenen Test automatisieren (mithilfe der Quellcode-Analyse können Sie trivialerweise Zeilennummern für ast.AssertKnoten in einem AST des Tests extrahieren ) und den bestätigten Test ausführen Verwenden Sie Debugger-Skriptbefehle und verwenden Sie den jumpBefehl, 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.

Martijn Pieters
quelle
7

Mit pytest --pdb können Sie ohne Codeänderung genau das erreichen, was Sie wollen .

Mit Ihrem Beispiel:

import pytest
def test_abc():
    a = 9
    assert a == 10, "some error message"

Führen Sie mit --pdb aus:

py.test --pdb
collected 1 item

test_abc.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_abc():
        a = 9
>       assert a == 10, "some error message"
E       AssertionError: some error message
E       assert 9 == 10

test_abc.py:4: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /private/tmp/a/test_abc.py(4)test_abc()
-> assert a == 10, "some error message"
(Pdb) p a
9
(Pdb)

Sobald ein Test fehlschlägt, können Sie ihn mit dem eingebauten Python-Debugger debuggen. Wenn Sie continuemit dem Debuggen fertig sind, können Sie mit den restlichen Tests fortfahren.

gnvk
quelle
Wird dies beendet, wenn der Testfall fehlschlägt oder der Testschritt fehlschlägt?
Nitesh
Bitte überprüfen Sie die verlinkten Dokumente: doc.pytest.org/en/latest/…
gnvk
Exzellente Idee. Wenn Sie jedoch --pdb verwenden, wird der Testfall bei jedem Fehler angehalten. Kann ich zur Laufzeit entscheiden, bei welchem ​​Fehler ich den Testfall
Nitesh
5

Wenn Sie PyCharm verwenden, können Sie einen Ausnahme-Haltepunkt hinzufügen, um die Ausführung anzuhalten, wenn eine Bestätigung fehlschlägt. Wählen Sie Haltepunkte anzeigen (STRG-UMSCHALT-F8) und fügen Sie einen Ausnahmehandler für AssertionError hinzu. Beachten Sie, dass dies die Ausführung der Tests verlangsamen kann.

Andernfalls haben Sie einige Optionen , wenn es Ihnen nichts ausmacht, am Ende jedes fehlgeschlagenen Tests (kurz vor dem Fehler) anzuhalten, anstatt an dem Punkt, an dem die Behauptung fehlschlägt. Beachten Sie jedoch, dass zu diesem Zeitpunkt möglicherweise bereits verschiedene Bereinigungscodes ausgeführt wurden, z. B. das Schließen von Dateien, die im Test geöffnet wurden. Mögliche Optionen sind:

  1. Mit der Option --pdb können Sie pytest anweisen, Sie bei Fehlern in den Debugger zu verschieben .

  2. Sie können den folgenden Dekorator definieren und damit jede relevante Testfunktion dekorieren. (Neben der Nachricht protokolliert, können Sie auch einen Start pdb.post_mortem an dieser Stelle oder sogar eine interaktive code.interact mit den Einheimischen des Rahmens , wo die Ausnahme entstanden sind , wie beschrieben in dieser Antwort .)

from functools import wraps

def pause_on_assert(test_func):
    @wraps(test_func)
    def test_wrapper(*args, **kwargs):
        try:
            test_func(*args, **kwargs)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            # re-raise exception to make the test fail
            raise
    return test_wrapper

@pause_on_assert
def test_abc()
    a = 10
    assert a == 2, "some error message"
  1. Wenn Sie nicht jede Testfunktion manuell dekorieren wollen, können Sie stattdessen eine autouse Befestigung definieren , dass Prüft sys.last_value :
import sys

@pytest.fixture(scope="function", autouse=True)
def pause_on_assert():
    yield
    if hasattr(sys, 'last_value') and isinstance(sys.last_value, AssertionError):
        tkinter.messagebox.showinfo(sys.last_value)
Uri Granta
quelle
Ich mochte die Antwort mit Dekorateuren, aber es kann nicht dynamisch gemacht werden. Ich möchte dynamisch steuern, wann ich pause_on_assert möchte oder nicht. Gibt es dafür eine Lösung?
Nitesh
Auf welche Weise dynamisch? Wie bei einem einzelnen Schalter, um es überall zu aktivieren / deaktivieren? Oder eine Möglichkeit, es für jeden Test zu kontrollieren?
Uri Granta
Angenommen, ich führe einige Testfälle aus. In der Mitte musste ich bei einem Fehler eine Pause einlegen. Ich werde den Schalter aktivieren. Später habe ich zu jedem Zeitpunkt das Gefühl, dass ich den Schalter deaktivieren muss.
Nitesh
Ihr Dekorateur als Antwort: 2 wird bei mir nicht funktionieren, da mein Testfall mehrere Asserts enthält
Nitesh
In Bezug auf einen 'Switch' können Sie jede Implementierung von aktualisieren pause_on_assert , um aus einer Datei zu lesen, um zu entscheiden, ob Sie pausieren möchten oder nicht.
Uri Granta
4

Eine einfache Lösung, wenn Sie bereit sind, Visual Studio-Code zu verwenden, könnte darin bestehen, bedingte Haltepunkte zu verwenden .

Auf diese Weise können Sie Ihre Aussagen einrichten, zum Beispiel:

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Fügen Sie dann einen bedingten Haltepunkt in Ihre Assert-Zeile ein, der nur dann unterbrochen wird, wenn Ihre Assertion fehlschlägt:

Geben Sie hier die Bildbeschreibung ein

Nick Martin
quelle
@Nitesh - Ich denke, diese Lösung löst alle Ihre Probleme. Sie brechen nur, wenn eine Zusicherung fehlschlägt. Sie können den Code dort und dann debuggen und anschließend mit den verbleibenden Tests fortfahren. Die Einrichtung ist jedoch etwas umständlicher anfangs
Nick Martin