32

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

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

ClassTest Caseで、各test_MethodTest Stepです。

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

私が必要とtest_しているのは、1 つのクラス内の残りのメソッドを何らかの方法でスキップ (または xfail) し、残りのテスト ケースが実行されず、FAILED としてマークされないようにすることです (それは誤検知になるため)。

ありがとう!

更新:「それは悪い習慣です」という答えは探していません。そのように呼ぶことは非常に議論の余地があるためです。(各テスト クラスは独立しています。これで十分です)。

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

4

9 に答える 9

29

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

これは、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つのテストスレーブに卸売りで送信されるグループ/クラスを定義する方法を開発する必要があります。

于 2012-09-25T09:06:56.970 に答える
3

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

とにかく、ドキュメントを読むと、必要な機能のような機能が実装されていないようです(おそらくそれが有用であるとは考えられていなかったためです)。

回避策は、クラスに何らかの条件を設定するカスタムメソッドを呼び出してテストを「失敗」させ、各テストに「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つが失敗した場合、同じテストケース内の他の失敗したテストは誤検知と見なされるべきです...しかし、これは「手動で」行うことができます。出力を確認して、誤検知を見つけてください。これは退屈かもしれませんが/エラーが発生しやすいです。

于 2012-09-13T17:20:43.687 に答える
0

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

py.test --maxfail=1

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

于 2016-01-26T00:28:23.437 に答える
0

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()

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

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を確認してください。

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

于 2018-07-27T16:03:23.637 に答える
0

UPDATE: Please take a look at @hpk42 answer. His answer is less intrusive.

This is what I was actually looking for:

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

Now adding this to conftest.py or as a plugin solves my problem.
Also it's improved to STOP testing if the blocker test has failed. (meaning that the entire further tests are useless)

于 2012-09-24T18:41:01.080 に答える