web-dev-qa-db-fra.com

Pytest: comment passer le reste des tests de la classe s’il échoue?

Je crée les scénarios de test pour les tests Web à l'aide des frameworks Jenkins, Python, Selenium2 (webdriver) et Py.test.

Jusqu'à présent, j'organise mes tests dans la structure suivante:

chaque Classe est le scénario de test et chaquetest_ méthode est un étape de test .

Cette configuration fonctionne à merveille lorsque tout fonctionne correctement. Cependant, lorsqu'une étape se bloque, le reste des "étapes de test" deviennent fous. Je suis en mesure de contenir l'échec à l'intérieur de la classe (scénario de test) à l'aide de teardown_class(), mais je cherche des solutions pour améliorer cela.

Ce dont j'ai besoin, c'est en quelque sorte d'ignorer (ou xfail) le reste des méthodes test_ au sein d'une classe si l'une d'entre elles a échoué, de sorte que le reste des tests ne soit pas exécuté et marqué comme FAILED (car cela serait faux positif)

Merci!

UPDATE: Je ne cherche pas ou la réponse "c'est une mauvaise pratique", car l'appeler ainsi est très discutable. (chaque classe de test est indépendante - et cela devrait suffire).

UPDATE 2: Mettre la condition "si" dans chaque méthode de test n'est pas une option - est BEAUCOUP de travail répété. Ce que je recherche, c'est (peut-être), quelqu'un sait comment utiliser les hooks avec les méthodes de classe.

26
Alex Okrushko

J'aime l'idée générale de "l'étape du test". Je qualifierais cela de test "incrémental" et c'est plus logique dans les scénarios de test fonctionnel à mon humble avis.

Voici une implémentation qui ne dépend pas des détails internes de pytest (sauf pour les extensions de hook officielles). Copiez ceci dans votre conftest.py:

import pytest

def pytest_runtest_makereport(item, call):
    if "incremental" in item.keywords:
        if call.excinfo is not None:
            parent = item.parent
            parent._previousfailed = item

def pytest_runtest_setup(item):
    previousfailed = getattr(item.parent, "_previousfailed", None)
    if previousfailed is not None:
        pytest.xfail("previous test failed (%s)" % previousfailed.name)

Si vous avez maintenant un "test_step.py" comme ceci:

import pytest

@pytest.mark.incremental
class TestUserHandling:
    def test_login(self):
        pass
    def test_modification(self):
        assert 0
    def test_deletion(self):
        pass

puis cela ressemble à ceci (utiliser -rx pour signaler les raisons de xfail):

(1)hpk@t2:~/p/pytest/doc/en/example/teststep$ py.test -rx
============================= test session starts ==============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev17
plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov, timeout
collected 3 items

test_step.py .Fx

=================================== FAILURES ===================================
______________________ TestUserHandling.test_modification ______________________

self = <test_step.TestUserHandling instance at 0x1e0d9e0>

    def test_modification(self):
>       assert 0
E       assert 0

test_step.py:8: AssertionError
=========================== short test summary info ============================
XFAIL test_step.py::TestUserHandling::()::test_deletion
  reason: previous test failed (test_modification)
================ 1 failed, 1 passed, 1 xfailed in 0.02 seconds =================

J'utilise "xfail" ici parce que les sauts sont plutôt pour des environnements incorrects ou des dépendances manquantes, des versions d'interpréteur incorrectes. 

Edit: Notez que ni votre exemple ni le mien ne fonctionnerait directement avec les tests distribués. Pour cela, le plugin pytest-xdist doit trouver un moyen de définir les groupes/classes à envoyer en vente complète à un esclave de test au lieu du mode actuel, qui envoie généralement les fonctions de test d’une classe à différents esclaves.

25
hpk42
6
gbonetti

C'est généralement une mauvaise pratique de faire ce que tu fais. Chaque test doit être aussi indépendant que possible des autres, tout en dépendant complètement des résultats des autres tests.

Quoi qu'il en soit, en lisant les docs , il semble qu’une fonctionnalité comme celle que vous voulez n’est pas mise en œuvre (probablement parce que cela n’a pas été jugé utile).

Une solution de rechange pourrait consister à "échouer" vos tests en appelant une méthode personnalisée définissant une condition sur la classe et en marquant chaque test avec le décorateur "skipIf":

class MyTestCase(unittest.TestCase):
    skip_all = False

   @pytest.mark.skipIf("MyTestCase.skip_all")
   def test_A(self):
        ...
        if failed:
            MyTestCase.skip_all = True
  @pytest.mark.skipIf("MyTestCase.skip_all")
  def test_B(self):
      ...
      if failed:
          MyTestCase.skip_all = True

Ou vous pouvez effectuer ce contrôle avant d'exécuter chaque test et éventuellement appeler pytest.skip().

edit: Le marquage comme xfail peut être fait de la même manière, mais en utilisant les appels de fonction correspondants.

Probablement qu'au lieu de réécrire le code de la plaque de chaudière pour chaque test, vous pourriez écrire un décorateur (cela exigerait probablement que vos méthodes renvoient un "indicateur" si elles échouent ou non).

Quoi qu’il en soit, je tiens à souligner que, comme vous le dites, si l’un de ces tests échoue, les autres tests infructueux dans le même cas de test doivent être considérés comme faux positifs ... Mais vous pouvez le faire "par main". Il suffit de vérifier le résultat et de repérer les faux positifs. Même si cela peut être ennuyeux/sujet à l’erreur.

3
Bakuriu

Vous voudrez peut-être jeter un œil à pytest-dependency . C'est un plugin qui vous permet de sauter des tests si un autre test a échoué. Dans votre cas même, il semble que les tests incrémentiels dont gbonetti a parlé soient plus pertinents.

2
azmeuk

UPDATE: S'il vous plaît jeter un oeil à @ hpk42 answer. Sa réponse est moins intrusive.

C'est ce que je cherchais réellement:

from _pytest.runner import runtestprotocol
import pytest
from _pytest.mark import MarkInfo

def check_call_report(item, nextitem):
    """
    if test method fails then mark the rest of the test methods as 'skip'
    also if any of the methods is marked as 'pytest.mark.blocker' then
    interrupt further testing
    """
    reports = runtestprotocol(item, nextitem=nextitem)
    for report in reports:
        if report.when == "call":
            if report.outcome == "failed":
                for test_method in item.parent._collected[item.parent._collected.index(item):]:
                    test_method._request.applymarker(pytest.mark.skipif("True"))
                    if test_method.keywords.has_key('blocker') and isinstance(test_method.keywords.get('blocker'), MarkInfo):
                        item.session.shouldstop = "blocker issue has failed or was marked for skipping"
            break

def pytest_runtest_protocol(item, nextitem):
# add to the hook
    item.ihook.pytest_runtest_logstart(
        nodeid=item.nodeid, location=item.location,
    )
    check_call_report(item, nextitem)
    return True

Ajouter maintenant ceci à conftest.py ou en tant que plugin résout mon problème.
De plus, il est désormais préférable d’arrêter le test si le test blocker a échoué. (ce qui signifie que tous les autres tests sont inutiles)

0
Alex Okrushko

Ou tout simplement au lieu d'appeler py.test depuis cmd (ou tox ou ailleurs), appelez simplement:

py.test --maxfail=1

voir ici pour plus de changements: https://pytest.org/latest/usage.html

0
theQuestionMan

L'option pytest -x arrêtera le test après le premier échec: pytest -vs -x test_sample.py

0
Parth Naik

Pour compléter la réponse de hpk42 , vous pouvez également utiliser pytest-étapes pour effectuer des tests incrémentiels. Cela peut vous aider en particulier si vous souhaitez partager une sorte de résultats incrémentiels d'état/intermédiaires pas. 

Avec ce paquet, vous n'avez pas besoin de mettre toutes les étapes dans une classe (vous pouvez, mais ce n'est pas obligatoire), décorez simplement votre fonction "suite de tests" avec @test_steps:

from pytest_steps import test_steps

def step_a():
    # perform this step ...
    print("step a")
    assert not False  # replace with your logic

def step_b():
    # perform this step
    print("step b")
    assert not False  # replace with your logic

@test_steps(step_a, step_b)
def test_suite_no_shared_results(test_step):
    # Execute the step
    test_step()

Vous pouvez ajouter un paramètre steps_data à votre fonction de test si vous souhaitez partager un objet StepsDataHolder entre vos étapes.

import pytest
from pytest_steps import test_steps, StepsDataHolder

def step_a(steps_data):
    # perform this step ...
    print("step a")
    assert not False  # replace with your logic

    # intermediate results can be stored in steps_data
    steps_data.intermediate_a = 'some intermediate result created in step a'

def step_b(steps_data):
    # perform this step, leveraging the previous step's results
    print("step b")

    # you can leverage the results from previous steps... 
    # ... or pytest.skip if not relevant
    if len(steps_data.intermediate_a) < 5:
        pytest.skip("Step b should only be executed if the text is long enough")

    new_text = steps_data.intermediate_a + " ... augmented"  
    print(new_text)
    assert len(new_text) == 56

@test_steps(step_a, step_b)
def test_suite_with_shared_results(test_step, steps_data: StepsDataHolder):

    # Execute the step with access to the steps_data holder
    test_step(steps_data)

Enfin, vous pouvez automatiquement ignorer ou échouer une étape si une autre a échoué avec @depends_on. Consultez le documentation pour plus de détails.

(Je suis l'auteur de ce paquet au fait;))

0
smarie

Sur la base de la réponse de hpk42 , voici ma marque légèrement modifiée incremental qui rend les tests élémentaires xfail si le test précédent a échoué (mais pas s’il xfailed ou a été ignoré). Ce code doit être ajouté à conftest.py:

import pytest

try:
    pytest.skip()
except BaseException as e:
    Skipped = type(e)

try:
    pytest.xfail()
except BaseException as e:
    XFailed = type(e)

def pytest_runtest_makereport(item, call):
    if "incremental" in item.keywords:
        if call.excinfo is not None:
            if call.excinfo.type in {Skipped, XFailed}:
                return

            parent = item.parent
            parent._previousfailed = item

def pytest_runtest_setup(item):
    previousfailed = getattr(item.parent, "_previousfailed", None)
    if previousfailed is not None:
        pytest.xfail("previous test failed (%s)" % previousfailed.name)

Et ensuite, une collection de cas de test doit être marquée avec @pytest.mark.incremental:

import pytest

@pytest.mark.incremental
class TestWhatever:
    def test_a(self):  # this will pass
        pass

    def test_b(self):  # this will be skipped
        pytest.skip()

    def test_c(self):  # this will fail
        assert False

    def test_d(self):  # this will xfail because test_c failed
        pass

    def test_e(self):  # this will xfail because test_c failed
        pass
0
Aran-Fey