Jenkins、Python、Selenium2(webdriver)、およびPy.testフレームワークを使用して、webテストのテストケースを作成しています。
これまでのところ、私は次の構造でテストを整理しています:
eachClassisTest Caseand each_test_
_ methodはテストステップです。
この設定は、すべてが正常に機能している場合は非常に機能しますが、1つのステップがクラッシュすると、残りの「テストステップ」が狂います。 teardown_class()
の助けを借りて、クラス(テストケース)内に障害を含めることができますが、これを改善する方法を調べています。
私が必要なのは、1つのクラス内の_test_
_メソッドの1つが失敗した場合、それを何らかの理由でスキップ(またはxfail)して、残りのテストケースが実行されず、FAILEDとマークされることです(これは誤検知)
ありがとう!
UPDATE:そのように呼ぶのは非常に議論の余地があるので、私は見ていません。 (各テストクラスは独立しています-それで十分です)。
UPDATE 2:各テストメソッドに "if"条件を置くことはオプションではありません-繰り返しの作業の多くです。私が探しているのは、(多分) フック をクラスのメソッドに使用する方法を誰かが知っていることです。
私は一般的な「テストステップ」の考え方が好きです。私はそれを「インクリメンタル」テストと呼び、機能テストシナリオ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つのテストスレーブにセール全体を送信するグループ/クラスを定義する方法を拡張する必要があります。
Nの失敗後にテストの実行を停止したい場合(特定のテストクラスではない)コマンドラインオプションpytest --maxfail=N
は次の方法です: https://docs.pytest.org/en/latest/usage.html#stopping-after-the-first-or-n-failures
代わりに、複数のステップで構成されるテストのいずれかが失敗した場合にテストを停止したい場合(そして他のテストの実行を継続する場合)、すべてを実行する必要がありますクラスでのステップ、および@pytest.mark.incremental
そのクラスのデコレータとconftest.pyを編集して、ここに示すコードを含めます https://docs.pytest.org/en/latest/example/simple.html#incremental-testing-test-steps 。
一般的に、あなたが何をしているのかは悪い習慣です。各テストは、他のテストの結果に完全に依存する一方で、他のテストから可能な限り独立している必要があります。
とにかく、 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つが失敗した場合、同じテストケースの他の失敗したテストは誤検知と見なされるべきであることを指摘したいと思います。出力を確認して、誤検知を見つけます。これは退屈かもしれませんが、エラーが発生しやすいです。
pytest-dependency を確認してください。他のテストが失敗した場合に、いくつかのテストをスキップできるプラグインです。あなたの場合は、gbonettiが議論したインクリメンタルテストの方が関連性が高いようです。
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
テストが失敗した場合にテストを停止するように改善されました。 (以降のテスト全体が役に立たないことを意味します)
または、cmd(またはtoxまたはwherever)からpy.testを呼び出す代わりに、次のように呼び出します。
py.test --maxfail=1
その他のスイッチについては、こちらをご覧ください: https://pytest.org/latest/usage.html
pytest -x
オプションは、最初の失敗後にテストを停止します:pytest -vs -x test_sample.py
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 を確認してください。
(ちなみに私はこのパッケージの作者です;))
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