Подтвердить что ты не робот

Как пропустить остальные тесты в классе, если один провалился?

Я создаю тестовые примеры для веб-тестов с использованием Jenkins, Python, Selenium2 (webdriver) и фреймворков Py.test.

Пока я организую свои тесты в следующей структуре:

каждый Класс является Test Case, и каждый test_ метод - это Шаг теста.

Эта настройка работает БОЛЬШОЕ, когда все работает нормально, однако когда один шаг разбивается, остальные "тестовые шаги" сходят с ума. Я могу содержать сбой внутри класса (тестовый пример) с помощью teardown_class(), однако я изучаю, как улучшить это.

Мне нужно как-то пропускать (или xfail) остальные методы test_ в пределах одного класса, если один из них потерпел неудачу, так что остальные тестовые примеры не выполняются и помечены как FAILED (так как это быть ложным)

Спасибо!

ОБНОВЛЕНИЕ: Я не ищу или не отвечаю "плохая практика", так как называть его таким образом очень сложно. (каждый тестовый класс независим - и этого должно быть достаточно).

ОБНОВЛЕНИЕ 2: Включение условия "если" в каждом тестовом методе не является вариантом - это многократная повторная работа. Я ищу (может быть) кто-то знает, как использовать hooks для методов класса.

4b9b3361

Ответ 1

Мне нравится общая идея "тест-шаг". Я бы назвал это "инкрементным" тестированием, и оно наиболее целесообразно в сценариях функционального тестирования.

Вот реализация, которая не зависит от внутренних деталей 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)[email protected]:~/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 необходимо разработать способ определения групп/классов, которые будут отправляться на полную продажу одному тестирующему ведомому устройству вместо текущего режима, который обычно отправляет тестовые функции класса различным ведомым.

Ответ 2

Ответ 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 может быть выполнена аналогичным образом, но с использованием соответствующих вызовов функций.

Возможно, вместо того, чтобы переписывать код котельной плиты для каждого теста, вы можете написать декоратор (это, вероятно, потребует, чтобы ваши методы вернули "флаг", если они потерпели неудачу или нет).

В любом случае, я хотел бы указать, что, как вы заявляете, если один из этих тестов терпит неудачу, то другие неудачные тесты в одном и том же тестовом случае должны считаться ложноположительными... но вы можете сделать это "вручную". Просто проверьте вывод и укажите ложные срабатывания. Даже если это может быть скучно. /error склонны.

Ответ 4

Возможно, вы захотите взглянуть на pytest-зависимость. Это плагин, который позволяет пропустить некоторые тесты, если какой-то другой тест не прошел. В вашем случае кажется, что дополнительные тесты, которые обсуждали gbonetti, более актуальны.

Ответ 5

Или просто просто вместо вызова py.test из cmd (или tox или где бы то ни было), просто позвоните:

py.test --maxfail=1

см. здесь для большего количества переключателей: https://pytest.org/latest/usage.html

Ответ 6

ОБНОВЛЕНИЕ: Обратите внимание на ответ @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 или как плагин решает мою проблему.
Также он улучшился для тестирования STOP, если тест blocker не удался. (что означает, что все дальнейшие тесты бесполезны)

Ответ 7

Чтобы дополнить ответ hpk42, вы также можете использовать pytest-шаги для выполнения инкрементного тестирования, это может помочь вам, в частности, если вы хотите поделиться каким-либо инкрементным состоянием/промежуточными результатами между шагами.

С этим пакетом вам не нужно помещать все шаги в классе (вы можете, но это не обязательно), просто украсьте свою функцию "test suite" с помощью @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 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, проверьте подробности в документации.

(Кстати я автор этого пакета;))

Ответ 8

Опция pytest -x останавливает тестирование после первого сбоя: pytest -vs -x test_sample.py

Ответ 9

Основываясь на ответе hpk42, здесь моя слегка измененная incremental метка, которая делает тестовые случаи xfail, если предыдущий тест не удался (но не если он 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