web-dev-qa-db-ja.com

失敗した場合、クラスの残りのテストをスキップするにはどうすればよいですか?

Jenkins、Python、Selenium2(webdriver)、およびPy.testフレームワークを使用して、webテストのテストケースを作成しています。

これまでのところ、私は次の構造でテストを整理しています:

eachClassisTest Caseand each_test__ methodテストステップです。

この設定は、すべてが正常に機能している場合は非常に機能しますが、1つのステップがクラッシュすると、残りの「テストステップ」が狂います。 teardown_class()の助けを借りて、クラス(テストケース)内に障害を含めることができますが、これを改善する方法を調べています。

私が必要なのは、1つのクラス内の_test__メソッドの1つが失敗した場合、それを何らかの理由でスキップ(またはxfail)して、残りのテストケースが実行されず、FAILEDとマークされることです(これは誤検知)

ありがとう!

UPDATE:そのように呼ぶのは非常に議論の余地があるので、私は見ていません。 (各テストクラスは独立しています-それで十分です)。

UPDATE 2:各テストメソッドに "if"条件を置くことはオプションではありません-繰り返しの作業の多くです。私が探しているのは、(多分) フック をクラスのメソッドに使用する方法を誰かが知っていることです。

26
Alex Okrushko

私は一般的な「テストステップ」の考え方が好きです。私はそれを「インクリメンタル」テストと呼び、機能テストシナリオIMHOで最も理にかなっています。

以下は、pytestの内部の詳細に依存しない実装です(公式のフック拡張を除く)。これを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)

次のような「test_step.py」がある場合:

import pytest

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

それを実行すると、次のようになります(-rxを使用して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 =================

ここでは「xfail」を使用しています。スキップは、間違った環境や欠落している依存関係、間違ったインタープリターバージョンのためです。

編集:あなたの例も私の例も、分散テストでは直接機能しないことに注意してください。このため、pytest-xdistプラグインは、通常、クラスのテスト関数を別のスレーブに送信する現在のモードではなく、1つのテストスレーブにセール全体を送信するグループ/クラスを定義する方法を拡張する必要があります。

27
hpk42
7
gbonetti

一般的に、あなたが何をしているのかは悪い習慣です。各テストは、他のテストの結果に完全に依存する一方で、他のテストから可能な限り独立している必要があります。

とにかく、 the docs を読んでも、必要な機能のような機能は実装されていないようです(おそらく、有用とは見なされていなかったためです)。

回避策は、クラスにいくつかの条件を設定するカスタムメソッドを呼び出すテストを「失敗」させ、各テストを「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
_

または、各テストを実行する前にこの制御を行い、最終的にpytest.skip()を呼び出すことができます。

編集:xfailとしてマークすることは同じ方法で行うことができますが、対応する関数呼び出しを使用します。

おそらく、各テストのボイラープレートコードを書き直す代わりに、デコレーターを書くことができます(おそらく、メソッドが失敗したかどうかを示す「フラグ」をメソッドが返す必要があります)。

とにかく、私はあなたが述べたように、これらのテストの1つが失敗した場合、同じテストケースの他の失敗したテストは誤検知と見なされるべきであることを指摘したいと思います。出力を確認して、誤検知を見つけます。これは退屈かもしれませんが、エラーが発生しやすいです。

3
Bakuriu

pytest-dependency を確認してください。他のテストが失敗した場合に、いくつかのテストをスキップできるプラグインです。あなたの場合は、gbonettiが議論したインクリメンタルテストの方が関連性が高いようです。

3
azmeuk

PDATE: @ hpk42の回答をご覧ください。彼の答えはそれほど邪魔にならない。

これは私が実際に探していたものです:

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

これをconftest.pyに、またはpluginとして追加すると、問題が解決します。
また、blockerテストが失敗した場合にテストを停止するように改善されました。 (以降のテスト全体が役に立たないことを意味します)

0
Alex Okrushko

または、cmd(またはtoxまたはwherever)からpy.testを呼び出す代わりに、次のように呼び出します。

py.test --maxfail=1

その他のスイッチについては、こちらをご覧ください: https://pytest.org/latest/usage.html

0
theQuestionMan

pytest -xオプションは、最初の失敗後にテストを停止します:pytest -vs -x test_sample.py

0
Parth Naik

hpk42の答え を補完するために、 pytest-steps を使用してインクリメンタルテストを実行することもできます。これは、ある種のインクリメンタルな状態/中間結果を共有する場合に特に役立ちます。ステップの間。

このパッケージを使用すると、クラスにすべてのステップを配置する必要はありません(可能ですが、必須ではありません)。@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()

ステップ間でStepsDataHolderオブジェクトを共有する場合は、steps_dataパラメータをテスト関数に追加できます。

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)

最後に、@depends_onを使用して別のステップが失敗した場合、ステップを自動的にスキップまたは失敗させることができます。詳細は documentation を確認してください。

(ちなみに私はこのパッケージの作者です;))

0
smarie

hpk42の回答 に基づいて、前のテストが失敗した場合にテストケースをxfailにする、少し変更したincrementalマークをここに示します(ただし、xfailedまたはスキップされたの場合は例外です)。このコードを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)

そして、テストケースのコレクションは@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