web-dev-qa-db-fra.com

Est-il possible de changer le comportement de l'instruction assert de PyTest dans Python

J'utilise Python assert instructions pour faire correspondre le comportement réel et prévu. Je n'ai aucun contrôle sur ces derniers comme s'il y avait un cas de test d'erreur abandonné. Je veux prendre le contrôle de l'erreur d'assertion et Je veux définir si je veux abandonner le testcase en cas d'échec ou non.

Je veux également ajouter quelque chose comme s'il y a une erreur d'assertion, le cas de test doit être suspendu et l'utilisateur peut reprendre à tout moment.

Je ne sais pas comment faire ça

Exemple de code, nous utilisons ici pytest

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

Below is my expectation

Lorsque assert jette une assertionError, je devrais avoir la possibilité de mettre le testcase en pause et de pouvoir déboguer et reprendre plus tard. Pour faire une pause et reprendre, j'utiliserai le module tkinter. Je ferai une fonction d'assertion comme ci-dessous

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)

À l'avenir, je dois changer chaque déclaration d'assertion avec cette fonction comme

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

C'est trop d'effort pour moi car je dois faire des changements à des milliers d'endroits où j'ai utilisé assert. Existe-t-il un moyen simple de le faire dans pytest

Summary: J'ai besoin de quelque chose où je peux mettre le testcase en pause en cas d'échec, puis reprendre après le débogage. Je connais tkinter et c'est la raison pour laquelle je l'ai utilisé. Toute autre idée sera la bienvenue

Note: Le code ci-dessus n'est pas encore testé. Il peut également y avoir de petites erreurs de syntaxe

Edit: Merci pour les réponses. Étendre cette question un peu à l'avance maintenant. Et si je veux changer le comportement de l'assert. Actuellement, lorsqu'il y a une erreur d'assertion, le testcase se ferme. Que se passe-t-il si je veux choisir si j'ai besoin de la sortie de testcase en cas d'échec d'assertion particulier ou non. Je ne veux pas écrire de fonction d'assertion personnalisée comme mentionné ci-dessus car de cette façon je dois changer à plusieurs endroits

18
Nitesh

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 complètement des affirmations spécifiques, si vous en avez vraiment l'impression.

Gérer les exceptions, pas affirmer

Notez qu'un test qui échoue n'arrête pas normalement le pytest; uniquement si vous avez activé le lui dire explicitement de quitter après un certain nombre d'échecs . De plus, les tests échouent car une exception est déclenchée; assert lève 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 modifier assert.

Cependant, une assertion défaillante va terminer le test individuel. En effet, une fois qu'une exception est déclenchée en dehors d'un bloc try...except, Python déroule le cadre de la fonction actuelle, et il n'y a pas de retour possible.

Je ne pense pas que c'est ce que vous voulez, à en juger par votre description de votre _assertCustom() tente de 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 de gestion des é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 voir l'état de votre test. Vous pouvez interagir avec les variables locales du test, les variables globales et les variables locales et globales 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 commande q quit, alors pytest quitte également la course, en utilisant c pour continuer, le contrôle reviendra à pytest et le test suivant est exécuté.

Utilisation d'un débogueur alternatif

Vous n'êtes pas lié au débogueur pdb pour cela; vous pouvez définir un débogueur différent avec le commutateur --pdbcls. N'importe quelle implémentation pdb.Pdb() compatible fonctionnerait, y compris implémentation du débogueur IPython , ou la plupart des autres Python débogueurs (le débogueur pudb nécessite que le commutateur -s soit utilisé, ou un plugin spécial ). Le commutateur prend un module et une classe, par exemple pour utilisez pudb vous pouvez utiliser:

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

Vous pouvez utiliser cette fonctionnalité pour écrire votre propre classe wrapper autour de 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 pdb.post_mortem() fait :

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

Ici, t est un objet traceback . Lorsque p.interaction(None, t) retourne, pytest continue avec le test suivant, à moins quep.quitting Soit défini sur True (auquel point pytest puis sort).

Voici un exemple d'implémentation qui indique que nous refusons de déboguer et renvoie immédiatement, sauf si le test a soulevé 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)

Quand 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 niveau suivant est le pytest débogage et interaction hooks ; 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 la saisie du 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 bannière >>> entering PDB >>> 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épendant ​​du débogueur que vous utilisez! Vous pouvez accéder à l'implémentation interne en y accéder par son nom ; le plugin de hook interne pour cela s'appelle pdbinvoke. Pour l'empêcher de fonctionner, vous devez 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 mettre dans n'importe lequel des plugins d'emplacements sont chargés ; Je l'ai mis dans 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 plugin interne TerminalReporter 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 le crochet pytest_exception_interact Via un autre crochet, pytest_configure() , mais en s'assurant qu'il s'exécute suffisamment tard (en utilisant @pytest.hookimpl(trylast=True) ) pour pouvoir désinscrire le plugin interne pdbinvoke. Lorsque le hook est appelé, l'exemple teste par rapport à l'objet call.exceptinfo ; vous pouvez également vérifier le nœud ou le rapport aussi.

Avec l'exemple de code ci-dessus en place dans demo/conftest.py, L'échec du test test_ham Est ignoré, seul l'échec du test test_spam, Qui soulève ValueError, entraîne la déboguer l'ouverture d'invite:

$ 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 tout 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'argument node) et un accès direct à l'exception levée (via l'instance call.excinfoExceptionInfo).

Notez que des plugins de débogage pytest spécifiques (tels que pytest-pudb Ou pytest-pycharm) Enregistrent leur propre hooksp pytest_exception_interact. Une implémentation plus complète devrait boucler sur tous les plugins dans le gestionnaire de plugins pour remplacer automatiquement les plugins arbitraires, 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 des tests ayant é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 que les échecs disparaissent complètement, vous pouvez utiliser un hook différent: pytest_runtest_call() .

Lorsque pytest exécute des tests, il exécute le test via le hook 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 hook pytest_exception_interact() susmentionné est appelé. Donc, tout ce que vous devez faire est de changer le résultat produit par ce crochet; 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 hook wrapper . 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 hook wrapper et vous avez accès au hook result , 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 gestion standard de try...except.

Alors, comment faites-vous un test réussi? Vous avez 3 options de base:

  • Vous pouvez marquer le test comme un échec prév, en appelant pytest.xfail() dans l'encapsuleur.
  • 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 méthode outcome.force_result() ; 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 n'augmentent pas ValueError, comme ignoré:

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é l'indicateur -r a Pour indiquer plus clairement que test_ham A été ignoré maintenant.

Si vous remplacez l'appel pytest.skip() 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 outcome.force_result([]) le 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é avec [NOTRUN] Ou [XFAIL]) Mais vous êtes libre d'utiliser tout autre format de message que vous vouloir.

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 déclarations d'assertion individuelles

Si vous voulez modifier assert tests dans un test, alors vous vous préparez pour beaucoup plus de travail. Oui, c'est techniquement ​​possible, mais seulement en réécrivant le code même que Python va exécuter au moment de la compilation.

Lorsque vous utilisez pytest, c'est en fait déjà fait. Pytest réécrit les instructions assert 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 les arbres de syntaxe abstraite de Python fonctionnent. Si vous le faites, vous pourrait ​​monkeypatch ce module pour y ajouter vos propres modifications, y compris entourer le assert avec un gestionnaire try...except AssertionError:.

Cependant , vous ne pouvez pas simplement désactiver ou ignorer les assertions de manière sélective, car les instructions suivantes peuvent facilement dépendre de l'état (dispositions spécifiques des objets, ensemble de variables, etc.) qu'une assertion ignorée était censée se prémunir. Si une assertion teste que foo n'est pas None, alors une assertion ultérieure s'appuie sur foo.bar Pour exister, alors vous exécuterez simplement un AttributeError là, etc. Tenez-vous à relancer l'exception, si vous avez besoin de suivre cette voie.

Je ne vais pas entrer dans les détails de la réécriture de 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 à la état du test au point d'échec de l'assertion de toute façon.

Notez que si vous voulez le faire, vous n'avez pas besoin d'utiliser eval() (qui ne fonctionnerait pas de toute façon, assert est une instruction, vous devez donc utiliser exec() à la place), vous n'auriez pas non plus à exécuter l'assertion deux fois (ce qui peut entraîner des problèmes si l'expression utilisée dans l'état d'assertion est modifiée). Vous devez plutôt incorporer le nœud ast.Assert À l'intérieur d'un nœud ast.Try Et attacher un gestionnaire except qui utilise un nœud ast.Raise Vide pour relancer l'exception qui a été interceptée.

Utilisation du débogueur pour ignorer les instructions d'assertion.

Le débogueur Python permet en fait sauter les instructions, en utilisant la commande j/jump . Si vous savez à l'avant ​​qu'une assertion spécifique va échouer, vous pouvez l'utiliser pour la contourner. Vous pouvez exécuter vos tests avec --trace, qui ouvre le débogueur au début de chaque test, puis émettez un j <line after assert> pour le sauter lorsque le débogueur est mis en pause 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 crochet pytest_testrun_call() pour intercepter l'exception AssertionError
  • 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 le test again, mais cette fois en utilisant une sous-classe Pdb 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 par un c continuer.

Ou, au lieu d'attendre l'échec d'une assertion, vous pouvez automatiser la définition de points d'arrêt pour chaque assert trouvé dans un test (en utilisant à nouveau l'analyse du code source, vous pouvez extraire de manière triviale les numéros de ligne des nœuds ast.Assert Dans an an AST du test), exécutez le test affirmé en utilisant des commandes scriptées du débogueur et utilisez la commande jump pour ignorer l'assertion elle-même. Vous devrez 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 vaille la peine l'heure. 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 dans le code en cours de test, vous pouvez donc tout aussi bien vous concentrer sur le débogage du problème.

23
Martijn Pieters

Vous pouvez obtenir exactement ce que vous voulez sans aucune modification de code avec pytest --pdb .

Avec votre exemple:

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

Exécutez avec --pdb:

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)

Dès qu'un test échoue, vous pouvez le déboguer avec le débogueur intégré python. Si vous avez terminé le débogage, vous pouvez continue avec le reste des tests.

7
gnvk

Si vous utilisez PyCharm, vous pouvez ajouter un point d'arrêt d'exception pour suspendre l'exécution en cas d'échec d'une assertion. Sélectionnez Afficher les points d'arrêt (CTRL-SHIFT-F8) et ajoutez un gestionnaire d'exceptions de montée pour AssertionError. Notez que cela peut ralentir l'exécution des tests.

Sinon, si cela ne vous dérange pas de faire une pause à la fin de chaque test ayant échoué (juste avant son erreur) plutôt qu'au point où l'assertion échoue, alors vous avez quelques options. Notez cependant qu'à ce stade, plusieurs codes de nettoyage, tels que la fermeture des fichiers ouverts dans le test, peuvent déjà avoir été exécutés. Les options possibles sont:

  1. Vous pouvez dire à pytest de vous déposer dans le débogueur en cas d'erreur en utilisant l'option - pdb .

  2. Vous pouvez définir le décorateur suivant et décorer chaque fonction de test pertinente avec lui. (Outre l'enregistrement d'un message, vous pouvez également démarrer un pdb.post_mortem à ce stade, ou même un code.interact (---) interactif code.interact avec les sections locales de la trame d'où provient l'exception, comme décrit dans cette réponse .)

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. Si vous ne voulez pas décorer manuellement chaque fonction de test, vous pouvez définir à la place un appareil autouse qui inspecte 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)
4
Uri Granta

Une solution simple, si vous êtes prêt à utiliser Visual Studio Code, pourrait être d'utiliser points d'arrêt conditionnels .

Cela vous permettrait de configurer vos assertions, par exemple:

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

Ajoutez ensuite un point d'arrêt conditionnel dans votre ligne d'assertion qui ne se cassera qu'en cas d'échec de votre assertion:

enter image description here

4
Nick Martin