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

18

J'utilise des instructions d'assertion Python pour faire correspondre le comportement réel et attendu. Je n'ai pas de 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 définir si je veux abandonner le testcase en cas d'échec de l'assertion ou non.

Aussi, je veux 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 pytest ici

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 tkintermodule. Je ferai une fonction d'affirmation 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 danspytest

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 sais tkinteret 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 en 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

Nitesh
la source
3
Pourriez-vous nous donner un exemple de code de ce que vous aimeriez faire?
mrblewog
1
N'utilisez pas assertmais écrivez vos propres fonctions de vérification qui font ce que vous voulez.
molbdnilo
Pourquoi ne pas insérer assert dans le bloc try et le message d'erreur dans except ?
Prathik Kini
1
Cela ressemble à ce que vous voulez vraiment utiliser pytestpour vos cas de test. Il prend en charge l' utilisation assert et sauter des tests ainsi que de nombreuses autres fonctionnalités qui facilitent l' écriture des suites de test plus facile.
blubberdiblub
1
Ne serait-il pas assez simple d'écrire un outil simple qui remplacerait mécaniquement chaque élément assert cond, "msg"de votre code _assertCustom("assert cond, 'msg'")? sedUn monoligne pourrait probablement le faire.
NPE

Réponses:

23

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; assertaugmente AssertionErrormais 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...exceptbloc, 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 --pdbcommutateur 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 qcommande quit, pytest quitte également la course, en utilisant cfor 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 pdbdébogueur pour cela; vous pouvez définir un débogueur différent avec le --pdbclscommutateur. 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 -scommutateur soit utilisé ou un plugin spécial ). Le commutateur prend un module et une classe, par exemple, pour utiliser, pudbvous pouvez utiliser:

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

Vous pouvez utiliser cette fonctionnalité pour écrire votre propre classe wrapper Pdbqui renvoie simplement immédiatement si l'échec spécifique n'est pas quelque chose qui vous intéresse. pytestUtilise Pdb()exactement comme le pdb.post_mortem()fait :

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

Voici tun objet traceback . Lorsque p.interaction(None, t)revient, pytestcontinue 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_typepour 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 TerminalReporterplugin 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_interacthook 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 pdbinvokeplugin interne . Lorsque le crochet est appelé, l'exemple teste l' call.exceptinfoobjet ; 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' nodeargument) et l'accès direct à l'exception levée (via l' call.excinfo ExceptionInfoinstance).

Notez que des plugins de débogage pytest spécifiques (tels que pytest-pudbou pytest-pycharm) enregistrent leur propre pytest_exception_interacthooksp. 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_pluginet 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 Noneou 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...exceptgestion 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.Exceptionet 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.pyla 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 adrapeau 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 asserttests 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 assertinstructions 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.pycode 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 assertavec 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 foone l'est pas None, alors une assertion plus récente repose sur l' foo.barexistence, alors vous rencontrerez simplement un AttributeErrorlà-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 assertsici, 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, assertest 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.Assertnœud à l'intérieur d'un ast.Trynœud et attachez un gestionnaire except qui utilise un ast.Raisenœ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' AssertionErrorexception
  • 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 Pdbsous-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 cpoursuite.

Ou, au lieu d'attendre l'échec d'une assertion, vous pouvez automatiser la définition de points d'arrêt pour chacun asserttrouvé dans un test (à nouveau en utilisant l'analyse du code source, vous pouvez extraire trivialement les numéros de ligne des ast.Assertnœuds dans un AST du test), exécuter le test affirmé à l'aide de commandes de script du débogueur et utilisez la jumpcommande 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.

Martijn Pieters
la source
7

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 python intégré. Si vous avez terminé le débogage, vous pouvez le faire continueavec le reste des tests.

gnvk
la source
Cela s'arrêtera-t-il lorsque le scénario de test échoue ou que l'étape de test échoue?
Nitesh
Veuillez consulter les documents liés: doc.pytest.org/en/latest/…
gnvk
Excellente idée. Mais si vous utilisez --pdb, testcase s'arrêtera à chaque échec. puis-je décider au moment de l'exécution à quel échec je veux mettre le test en pause
Nitesh
5

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 qui échoue (juste avant qu'il ne fasse une erreur) plutôt qu'au moment où l'assertion échoue, 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. ( En plus de se connecter un message, vous pouvez également démarrer un pdb.post_mortem à ce stade, ou même un outil interactif code.interact avec les habitants du cadre où l'exception est originaire, 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)
Uri Granta
la source
J'ai aimé la réponse des décorateurs mais cela ne peut pas être fait de manière dynamique. Je veux contrôler dynamiquement quand je veux faire une pause_on_assert ou non. Existe-t-il une solution à ça?
Nitesh
Dynamique de quelle manière? Comme dans un seul interrupteur pour l'activer / le désactiver partout? Ou un moyen de le contrôler pour chaque test?
Uri Granta
Supposons que je lance des tests. Au milieu, j'ai eu besoin de faire une pause en cas d'échec. Je vais activer le commutateur. Plus tard, à tout moment, je sens que je dois désactiver le commutateur.
Nitesh
Votre décorateur en réponse: 2 ne fonctionnera pas pour moi car mon test aura plusieurs assertions
Nitesh
En ce qui concerne un «commutateur», vous pouvez mettre à jour l'une ou l'autre implémentation de pause_on_assertpour lire dans un fichier pour décider de suspendre ou non.
Uri Granta
4

Une solution simple, si vous souhaitez utiliser Visual Studio Code, pourrait être d'utiliser des 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:

entrez la description de l'image ici

Nick Martin
la source
@Nitesh - Je pense que cette solution résout tous vos problèmes, vous ne vous arrêtez que lorsqu'une assertion échoue, vous pouvez déboguer le code ici et ensuite et vous pouvez continuer avec les tests restants par la suite ... Quoiqu'il soit un peu plus lourd à configurer initialement
Nick Martin