Vous utilisez pytest
, ce qui vous offre de nombreuses options pour interagir avec les tests qui échouent. Il vous donne des options de ligne de commande et plusieurs crochets pour rendre cela possible. Je vais vous expliquer comment utiliser chacun et où vous pouvez faire des personnalisations pour répondre à vos besoins de débogage spécifiques.
J'entrerai également dans des options plus exotiques qui vous permettraient d'ignorer entièrement des assertions spécifiques, si vous vous sentez vraiment obligé.
Gérer les exceptions, pas affirmer
Notez qu'un test qui échoue n'arrête pas normalement pytest; uniquement si vous avez activé le explicitement lui dire de quitter après un certain nombre d'échecs . De plus, les tests échouent car une exception est déclenchée; assert
augmente AssertionError
mais ce n'est pas la seule exception qui fera échouer un test! Vous voulez contrôler la façon dont les exceptions sont gérées, pas les modifier assert
.
Cependant, une assertion défaillante mettra fin au test individuel. En effet, une fois qu'une exception est levée en dehors d'un try...except
bloc, Python déroule le cadre de fonction actuel, et il n'y a pas de retour en arrière.
Je ne pense pas que c'est ce que vous voulez, à en juger par votre description de vos _assertCustom()
tentatives pour relancer l'assertion, mais je vais néanmoins discuter de vos options plus bas.
Débogage post-mortem dans pytest avec pdb
Pour les différentes options pour gérer les échecs dans un débogueur, je vais commencer par le --pdb
commutateur de ligne de commande , qui ouvre l'invite de débogage standard en cas d'échec d'un test (sortie élidée pour plus de concision):
$ 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
[ ... ]
Avec ce commutateur, lorsqu'un test échoue, pytest démarre une session de débogage post-mortem . C'est essentiellement exactement ce que vous vouliez; pour arrêter le code au moment où un test a échoué et ouvrez le débogueur pour consulter l'état de votre test. Vous pouvez interagir avec les variables locales du test, les globaux et les locaux et globaux de chaque trame de la pile.
Ici, pytest vous donne un contrôle total sur la sortie ou non après ce point: si vous utilisez la q
commande quit, pytest quitte également la course, en utilisant c
for continue, le contrôle reviendra à pytest et le prochain test sera exécuté.
Utilisation d'un débogueur alternatif
Vous n'êtes pas lié au pdb
débogueur pour cela; vous pouvez définir un débogueur différent avec le --pdbcls
commutateur. Toute implémentation pdb.Pdb()
compatible fonctionnerait, y compris l' implémentation du débogueur IPython ou la plupart des autres débogueurs Python (le débogueur pudb nécessite que le -s
commutateur soit utilisé ou un plugin spécial ). Le commutateur prend un module et une classe, par exemple, pour utiliser, pudb
vous pouvez utiliser:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Vous pouvez utiliser cette fonctionnalité pour écrire votre propre classe wrapper Pdb
qui renvoie simplement immédiatement si l'échec spécifique n'est pas quelque chose qui vous intéresse. pytest
Utilise Pdb()
exactement comme le pdb.post_mortem()
fait :
p = Pdb()
p.reset()
p.interaction(None, t)
Voici t
un objet traceback . Lorsque p.interaction(None, t)
revient, pytest
continue avec le test suivant, sauf si p.quitting
est défini sur True
(à quel point pytest puis se termine).
Voici un exemple d'implémentation qui indique que nous refusons de déboguer et renvoie immédiatement, sauf si le test est déclenché ValueError
, enregistré sous 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)
Lorsque j'utilise ceci avec la démo ci-dessus, c'est la sortie (encore une fois, élidée par souci de concision):
$ 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)
Les introspections ci-dessus sys.last_type
pour déterminer si l'échec est «intéressant».
Cependant, je ne peux pas vraiment recommander cette option sauf si vous voulez écrire votre propre débogueur en utilisant tkInter ou quelque chose de similaire. Notez que c'est une grande entreprise.
Échecs de filtrage; choisissez et choisissez quand ouvrir le débogueur
Le prochain niveau est le pytest débogage et interaction crochets ; ce sont des points d'ancrage pour les personnalisations de comportement, pour remplacer ou améliorer la façon dont pytest gère normalement des choses comme la gestion d'une exception ou l'entrée dans le débogueur via pdb.set_trace()
ou breakpoint()
(Python 3.7 ou plus récent).
L'implémentation interne de ce hook est également responsable de l'impression de la >>> entering PDB >>>
bannière ci-dessus, donc l'utilisation de ce hook pour empêcher le débogueur de fonctionner signifie que vous ne verrez pas du tout cette sortie. Vous pouvez avoir votre propre hook puis déléguer au hook d'origine lorsqu'un échec de test est «intéressant», et donc filtrer les échecs de test indépendamment du débogueur que vous utilisez! Vous pouvez accéder à l'implémentation interne en y accédant par son nom ; le plugin de hook interne pour cela est nommé pdbinvoke
. Pour l'empêcher de fonctionner, vous devez le désinscrire , mais enregistrer une référence, nous pouvons l'appeler directement si nécessaire.
Voici un exemple d'implémentation d'un tel crochet; vous pouvez le placer dans n'importe quel emplacement depuis lequel les plugins sont chargés ; Je l'ai mis 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)
Le plugin ci-dessus utilise le TerminalReporter
plugin interne pour écrire des lignes sur le terminal; cela rend la sortie plus propre lors de l'utilisation du format d'état de test compact par défaut et vous permet d'écrire des choses sur le terminal même avec la capture de sortie activée.
L'exemple enregistre l'objet plugin avec pytest_exception_interact
hook via un autre hook, pytest_configure()
mais en s'assurant qu'il s'exécute suffisamment tard (en utilisant @pytest.hookimpl(trylast=True)
) pour pouvoir annuler l'enregistrement du pdbinvoke
plugin interne . Lorsque le crochet est appelé, l'exemple teste l' call.exceptinfo
objet ; vous pouvez également vérifier le nœud ou le rapport également.
Avec l'exemple de code ci-dessus en place dans demo/conftest.py
, l' test_ham
échec du test est ignoré, seul l' test_spam
échec du test, qui se déclenche ValueError
, entraîne l'ouverture de l'invite de débogage:
$ 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)
Pour réitérer, l'approche ci-dessus présente l'avantage supplémentaire que vous pouvez combiner cela avec n'importe quel débogueur qui fonctionne avec pytest , y compris pudb, ou le débogueur IPython:
$ 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>
Il a également beaucoup plus de contexte sur le test exécuté (via l' node
argument) et l'accès direct à l'exception levée (via l' call.excinfo
ExceptionInfo
instance).
Notez que des plugins de débogage pytest spécifiques (tels que pytest-pudb
ou pytest-pycharm
) enregistrent leur propre pytest_exception_interact
hooksp. Une implémentation plus complète devrait parcourir tous les plugins dans le gestionnaire de plugins pour remplacer automatiquement les plugins, en utilisant config.pluginmanager.list_name_plugin
et hasattr()
pour tester chaque plugin.
Faire disparaître complètement les échecs
Bien que cela vous donne un contrôle total sur le débogage d'un test qui a échoué, cela laisse toujours le test comme échoué même si vous avez choisi de ne pas ouvrir le débogueur pour un test donné. Si vous voulez faire des échecs disparaissent complètement, vous pouvez utiliser faire un crochet différent: pytest_runtest_call()
.
Lorsque pytest exécute des tests, il exécute le test via le crochet ci-dessus, qui devrait renvoyer None
ou déclencher une exception. À partir de cela, un rapport est créé, éventuellement une entrée de journal est créée et si le test a échoué, le pytest_exception_interact()
hook susmentionné est appelé. Donc, tout ce que vous devez faire est de changer le résultat que ce crochet produit; au lieu d'une exception, il ne devrait tout simplement pas retourner quoi que ce soit.
La meilleure façon de le faire est d'utiliser un crochet . Les enveloppes de crochet n'ont pas à effectuer le travail réel, mais ont plutôt la possibilité de modifier ce qui arrive au résultat d'un crochet. Tout ce que vous avez à faire est d'ajouter la ligne:
outcome = yield
dans votre implémentation de wrapper de hook et vous avez accès au résultat du hook , y compris l'exception de test via outcome.excinfo
. Cet attribut est défini sur un tuple de (type, instance, traceback) si une exception a été déclenchée dans le test. Vous pouvez également appeler outcome.get_result()
et utiliser la try...except
gestion standard .
Alors, comment faites-vous un test réussi? Vous avez 3 options de base:
- Vous pouvez marquer le test comme un échec attendu en appelant
pytest.xfail()
le wrapper.
- Vous pouvez marquer l'élément comme ignoré , ce qui prétend que le test n'a jamais été exécuté en premier lieu, en appelant
pytest.skip()
.
- Vous pouvez supprimer l'exception en utilisant la
outcome.force_result()
méthode ; définissez le résultat sur une liste vide ici (ce qui signifie: le hook enregistré n'a produit que None
), et l'exception est entièrement effacée.
Ce que vous utilisez dépend de vous. Assurez-vous de vérifier d'abord le résultat des tests ignorés et des échecs attendus car vous n'avez pas besoin de gérer ces cas comme si le test avait échoué. Vous pouvez accéder aux exceptions spéciales que ces options déclenchent via pytest.skip.Exception
et pytest.xfail.Exception
.
Voici un exemple d'implémentation qui marque les tests ayant échoué qui ne se déclenchent pas ValueError
, comme ignorés :
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")
Une fois mis dans conftest.py
la sortie devient:
$ 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 ======================
J'ai utilisé le -r a
drapeau pour clarifier ce qui a test_ham
été ignoré maintenant.
Si vous remplacez l' pytest.skip()
appel par pytest.xfail("[XFAIL] ignoring everything but ValueError")
, le test est marqué comme un échec attendu:
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
et en utilisant le outcome.force_result([])
marque comme passé:
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
C'est à vous de choisir celui qui vous convient le mieux. Pour skip()
et xfail()
j'ai imité le format de message standard (préfixé par [NOTRUN]
ou [XFAIL]
) mais vous êtes libre d'utiliser tout autre format de message que vous souhaitez.
Dans les trois cas, pytest n'ouvrira pas le débogueur pour les tests dont vous avez modifié le résultat en utilisant cette méthode.
Modification des instructions d'assertion individuelles
Si vous souhaitez modifier les assert
tests dans un test , vous vous préparez pour beaucoup plus de travail. Oui, c'est techniquement possible, mais seulement en réécrivant le code que Python va exécuter au moment de la compilation .
Lorsque vous utilisez pytest
, cela est déjà fait . Pytest réécrit les assert
instructions pour vous donner plus de contexte lorsque vos assertions échouent ; voir cet article de blog pour un bon aperçu de ce qui se fait exactement, ainsi que le _pytest/assertion/rewrite.py
code source . Notez que ce module fait plus de 1k lignes et nécessite que vous compreniez comment fonctionnent les arbres de syntaxe abstraite de Python . Si vous le faites, vous pouvez monkeypatch ce module pour y ajouter vos propres modifications, y compris entourer le assert
avec un try...except AssertionError:
gestionnaire.
Cependant , vous ne pouvez pas simplement désactiver ou ignorer les assertions de manière sélective, car les instructions ultérieures peuvent facilement dépendre de l'état (dispositions d'objets spécifiques, jeu de variables, etc.) contre lequel une assertion ignorée était censée se prémunir. Si une assertion teste qui foo
ne l'est pas None
, alors une assertion plus récente repose sur l' foo.bar
existence, alors vous rencontrerez simplement un AttributeError
là-bas, etc. Restez à relancer l'exception, si vous avez besoin de suivre cette voie.
Je ne vais pas entrer dans les détails sur la réécriture asserts
ici, car je ne pense pas que cela vaille la peine, étant donné la quantité de travail nécessaire, et avec le débogage post mortem vous donnant accès à l'état du test à la point d'échec de l'assertion de toute façon .
Notez que si vous voulez faire cela, vous n'avez pas besoin d'utiliser eval()
(ce qui ne fonctionnerait pas de toute façon, assert
est une instruction, vous devriez donc utiliser à la exec()
place), et vous n'auriez pas à exécuter l'assertion deux fois (qui peut entraîner des problèmes si l'expression utilisée dans l'état d'assertion est modifiée). Au lieu de cela, vous incorporez le ast.Assert
nœud à l'intérieur d'un ast.Try
nœud et attachez un gestionnaire except qui utilise un ast.Raise
nœud vide, relancez l'exception qui a été interceptée.
Utilisation du débogueur pour ignorer les instructions d'assertion.
Le débogueur Python vous permet en fait d' ignorer les instructions à l'aide de la commande j
/jump
. Si vous savez à l' avance qu'une affirmation spécifique va échouer, vous pouvez l' utiliser pour le contourner. Vous pouvez exécuter vos tests avec --trace
, ce qui ouvre le débogueur au début de chaque test , puis émettez un j <line after assert>
pour l'ignorer lorsque le débogueur est suspendu juste avant l'assertion.
Vous pouvez même automatiser cela. En utilisant les techniques ci-dessus, vous pouvez créer un plugin de débogage personnalisé qui
- utilise le
pytest_testrun_call()
crochet pour attraper l' AssertionError
exception
- extrait le numéro de ligne «incriminé» de la trace, et peut-être avec une analyse de code source détermine les numéros de ligne avant et après l'assertion requise pour exécuter un saut réussi
- exécute à nouveau le test , mais cette fois en utilisant une
Pdb
sous-classe qui définit un point d'arrêt sur la ligne avant l'assertion, et exécute automatiquement un saut à la seconde lorsque le point d'arrêt est atteint, suivi d'une c
poursuite.
Ou, au lieu d'attendre l'échec d'une assertion, vous pouvez automatiser la définition de points d'arrêt pour chacun assert
trouvé dans un test (à nouveau en utilisant l'analyse du code source, vous pouvez extraire trivialement les numéros de ligne des ast.Assert
nœuds dans un AST du test), exécuter le test affirmé à l'aide de commandes de script du débogueur et utilisez la jump
commande pour ignorer l'assertion elle-même. Il faudrait faire un compromis; exécutez tous les tests sous un débogueur (ce qui est lent car l'interpréteur doit appeler une fonction de trace pour chaque instruction) ou n'appliquez cela qu'aux tests qui échouent et payez le prix de la réexécution de ces tests à partir de zéro.
Un tel plugin serait beaucoup de travail à créer, je ne vais pas écrire un exemple ici, en partie parce qu'il ne rentrerait pas dans une réponse de toute façon, et en partie parce que je ne pense pas que cela en vaille la peine . Je viens d'ouvrir le débogueur et de faire le saut manuellement. Une assertion défaillante indique un bogue dans le test lui-même ou le code en cours de test, vous pouvez donc tout aussi bien vous concentrer sur le débogage du problème.