32

我正在使用 Jenkins、Python、Selenium2(webdriver) 和 Py.test 框架为 Web 测试创建测试用例。

到目前为止,我正在按以下结构组织我的测试:

每个都是测试用例,每个test_方法都是一个测试步骤

当一切正常时,此设置工作得很好,但是当一个步骤崩溃时,其余的“测试步骤”就会发疯。在 的帮助下,我能够将失败包含在类(测试用例)中teardown_class(),但是我正在研究如何改进它。

如果其中一个方法失败,我需要以某种方式跳过(或 xfail)test_一个类中的其余方法,以便其余测试用例不会运行并标记为 FAILED(因为那将是误报)

谢谢!

更新:我没有寻找或回答“这是不好的做法”,因为这样称呼它是非常有争议的。(每个测试类都是独立的——这就足够了)。

更新 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 插件需要开发一种方法来定义组/类,以将其整体发送到一个测试从属设备,而不是当前模式,后者通常将一个类的测试功能发送到不同的从属设备。

于 2012-09-25T09:06:56.970 回答
11
于 2017-06-19T14:24:22.143 回答
4

pytest -x选项将在第一次失败后停止测试: pytest -vs -x test_sample.py

于 2018-08-09T09:50:23.307 回答
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可以以相同的方式完成,但使用相应的函数调用。

可能,您可以编写一个装饰器,而不是为每个测试重写样板代码(这可能需要您的方法返回一个“标志”,说明它们是否失败)。

无论如何,我想指出,正如您所说,如果其中一个测试失败,那么同一测试用例中的其他失败测试应被视为误报......但您可以“手动”执行此操作。只需检查输出并发现误报。即使这可能很无聊。/容易出错。

于 2012-09-13T17:20:43.687 回答
3

您可能想看看pytest-dependency。这是一个插件,如果其他一些测试失败,它允许您跳过一些测试。就您而言,gbonetti 讨论的增量测试似乎更相关。

于 2017-07-20T08:21:54.600 回答
1

根据hpk42 的回答,这是我稍作修改的incremental标记,如果先前的测试失败(但如果它xfailed或它被跳过,则不会使测试用例 xfail )。此代码必须添加到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
于 2018-11-01T21:14:34.013 回答
0

更新:请查看@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或作为插件解决了我的问题。
如果blocker测试失败,它也改进为停止测试。(意味着整个进一步的测试都是无用的)

于 2012-09-24T18:41:01.080 回答
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 回答