Вы используете pytest
, что дает вам широкие возможности для взаимодействия с ошибочными тестами. Это дает вам параметры командной строки и несколько хуков, чтобы сделать это возможным. Я объясню, как использовать каждый из них и где вы могли бы сделать настройки в соответствии с вашими конкретными потребностями отладки.
Я также расскажу о более экзотических опциях, которые позволили бы вам полностью пропустить конкретные утверждения, если вы действительно этого хотите.
Обрабатывать исключения, а не утверждать
Обратите внимание, что провальный тест обычно не останавливает pytest; только если вы включили явное указание ему выйти после определенного количества сбоев . Кроме того, тесты не проходят, потому что возникает исключение; assert
поднимает, AssertionError
но это не единственное исключение, которое приведет к провалу теста! Вы хотите контролировать обработку исключений, а не изменять их assert
.
Тем не менее, в случае неудачного утверждения будет завершен отдельный тест. Это потому, что как только исключение возникает за пределами try...except
блока, Python разматывает текущий фрейм функции, и возвращаться к нему уже нельзя.
Я не думаю, что это то, что вы хотите, судя по вашему описанию ваших _assertCustom()
попыток повторно выполнить утверждение, но, тем не менее, я буду обсуждать ваши варианты ниже.
Посмертная отладка в pytest с помощью pdb
Для различных вариантов обработки сбоев в отладчике я начну с --pdb
переключателя командной строки , который открывает стандартное приглашение отладки при сбое теста (выходные данные исключены для краткости):
$ mkdir demo
$ touch demo/__init__.py
$ cat << EOF > demo/test_foo.py
> def test_ham():
> assert 42 == 17
> def test_spam():
> int("Vikings")
> EOF
$ pytest demo/test_foo.py --pdb
[ ... ]
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(2)test_ham()
-> assert 42 == 17
(Pdb) q
Exit: Quitting debugger
[ ... ]
С помощью этого переключателя, когда тест не пройден, pytest начинает сеанс отладки после смерти . По сути, это именно то, что вы хотели; чтобы остановить код в точке неудачного теста и открыть отладчик, чтобы посмотреть на состояние вашего теста. Вы можете взаимодействовать с локальными переменными теста, глобальными переменными, а также локальными и глобальными переменными каждого кадра в стеке.
Здесь pytest дает вам полный контроль над тем, выходить или нет после этой точки: если вы используете команду q
quit, то pytest также завершает выполнение, использование c
для продолжения вернет управление в pytest и будет выполнен следующий тест.
Использование альтернативного отладчика
Вы не связаны с pdb
отладчиком для этого; Вы можете установить другой отладчик с помощью --pdbcls
переключателя. Будет работать любая pdb.Pdb()
совместимая реализация, включая реализацию отладчика IPython или большинство других отладчиков Python ( отладчик pudb требует использования -s
переключателя или специального плагина ). Коммутатор принимает модуль и класс, например, для использования pudb
вы можете использовать:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Вы можете использовать эту функцию , чтобы написать свой собственный класс - обертку вокруг , Pdb
которая просто возвращает немедленно , если отказ конкретных не то , что вы заинтересованы в том , pytest
использует Pdb()
так же , как pdb.post_mortem()
делает :
p = Pdb()
p.reset()
p.interaction(None, t)
Здесь t
находится объект трассировки . Когда p.interaction(None, t)
возвращается, pytest
продолжается со следующим тестом, если p.quitting
не установлено значение True
(после чего pytest затем завершается).
Вот пример реализации, которая распечатывает, что мы отказываемся отлаживать, и возвращает немедленно, если только тест не был запущен ValueError
и сохранен как demo/custom_pdb.py
:
import pdb, sys
class CustomPdb(pdb.Pdb):
def interaction(self, frame, traceback):
if sys.last_type is not None and not issubclass(sys.last_type, ValueError):
print("Sorry, not interested in this failure")
return
return super().interaction(frame, traceback)
Когда я использую это с вышеприведенной демонстрацией, это вывод (опять же, для краткости):
$ pytest test_foo.py -s --pdb --pdbcls=demo.custom_pdb:CustomPdb
[ ... ]
def test_ham():
> assert 42 == 17
E assert 42 == 17
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sorry, not interested in this failure
F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
Вышеупомянутые интроспективы sys.last_type
определяют, является ли неудача «интересной».
Однако я не могу порекомендовать эту опцию, если вы не хотите написать свой собственный отладчик, используя tkInter или что-то подобное. Обратите внимание, что это большое начинание.
Фильтрация сбоев; выбрать, когда открыть отладчик
Следующий уровень является pytest отладки и взаимодействия крюки ; это точки подключения для настройки поведения, чтобы заменить или улучшить то, как pytest обычно обрабатывает такие вещи, как обработка исключения или вход в отладчик через pdb.set_trace()
или breakpoint()
(Python 3.7 или новее).
Внутренняя реализация этого хука также отвечает за печать >>> entering PDB >>>
вышеупомянутого баннера, поэтому использование этого хука для предотвращения отладчика означает, что вы вообще не увидите этот вывод. Вы можете иметь свой собственный хук, а затем делегировать исходный хук, если неудачный тест «интересен», и таким образом фильтровать неудачи теста независимо от того, какой отладчик вы используете! Вы можете получить доступ к внутренней реализации, обратившись к ней по имени ; Внутренний подключаемый модуль для этого называется pdbinvoke
. Чтобы предотвратить его запуск, необходимо отменить его регистрацию, но сохранить ссылку. Мы можем вызывать ее напрямую по мере необходимости.
Вот пример реализации такого хука; Вы можете поместить это в любое место, из которого загружаются плагины ; Я вставил это в demo/conftest.py
:
import pytest
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
# unregister returns the unregistered plugin
pdbinvoke = config.pluginmanager.unregister(name="pdbinvoke")
if pdbinvoke is None:
# no --pdb switch used, no debugging requested
return
# get the terminalreporter too, to write to the console
tr = config.pluginmanager.getplugin("terminalreporter")
# create or own plugin
plugin = ExceptionFilter(pdbinvoke, tr)
# register our plugin, pytest will then start calling our plugin hooks
config.pluginmanager.register(plugin, "exception_filter")
class ExceptionFilter:
def __init__(self, pdbinvoke, terminalreporter):
# provide the same functionality as pdbinvoke
self.pytest_internalerror = pdbinvoke.pytest_internalerror
self.orig_exception_interact = pdbinvoke.pytest_exception_interact
self.tr = terminalreporter
def pytest_exception_interact(self, node, call, report):
if not call.excinfo. errisinstance(ValueError):
self.tr.write_line("Sorry, not interested!")
return
return self.orig_exception_interact(node, call, report)
Данный плагин использует внутренний TerminalReporter
плагин для записи строк в терминал; это делает вывод более чистым при использовании формата статуса компактного теста по умолчанию и позволяет записывать данные в терминал даже с включенным захватом вывода.
В этом примере объект плагина регистрируется с pytest_exception_interact
помощью ловушки через другую ловушку, pytest_configure()
но необходимо убедиться, что он работает достаточно поздно (используя @pytest.hookimpl(trylast=True)
), чтобы иметь возможность отменить регистрацию внутреннего pdbinvoke
плагина. Когда вызывается ловушка, пример проверяет call.exceptinfo
объект ; Вы также можете проверить узел или отчет тоже.
С учетом указанных выше примере кода на месте в demo/conftest.py
, то test_ham
сбой теста игнорируется, только test_spam
ошибки теста, который поднимает ValueError
, приводит к быстрой отладки открытия:
$ pytest demo/test_foo.py --pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
Чтобы повторить, вышеприведенный подход имеет дополнительное преимущество, заключающееся в том, что вы можете комбинировать его с любым отладчиком, который работает с pytest , включая pudb или отладчик IPython:
$ pytest demo/test_foo.py --pdb --pdbcls=IPython.core.debugger:Pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
1 def test_ham():
2 assert 42 == 17
3 def test_spam():
----> 4 int("Vikings")
ipdb>
В нем также гораздо больше контекста о том, какой тест выполнялся (через node
аргумент), и прямой доступ к вызванному исключению (через call.excinfo
ExceptionInfo
экземпляр).
Обратите внимание, что определенные плагины отладчика pytest (такие как pytest-pudb
или pytest-pycharm
) регистрируют свои собственные pytest_exception_interact
ловушки. Более полная реализация должна была бы перебрать все плагины в менеджере плагинов, чтобы автоматически переопределять произвольные плагины, используя config.pluginmanager.list_name_plugin
и hasattr()
для тестирования каждого плагина.
Делать неудачи уходят совсем
Хотя это дает вам полный контроль над неудачной отладкой теста, это все равно оставляет тест неудачным, даже если вы решили не открывать отладчик для данного теста. Если вы хотите , чтобы неудачи уйти в целом, вы можете использовать другой крючок: pytest_runtest_call()
.
Когда pytest запускает тесты, он запускает тест через описанный выше хук, который, как ожидается, вернет None
или вызовет исключение. Из этого создается отчет, при желании создается запись в журнале, и если проверка не пройдена, pytest_exception_interact()
вызывается вышеупомянутая ловушка. Так что все, что вам нужно сделать, это изменить результат, полученный этим хуком; вместо исключения он просто не должен ничего возвращать.
Лучший способ сделать это - использовать упаковщик крюка . Обертки крючков не должны выполнять фактическую работу, но вместо этого им предоставляется возможность изменить то, что происходит с результатом крючка. Все, что вам нужно сделать, это добавить строку:
outcome = yield
в вашей реализации обработчика ловушек, и вы получите доступ к результату ловушки , включая исключение теста через outcome.excinfo
. Этот атрибут имеет значение кортеж (тип, экземпляр, трассировка), если в тесте возникло исключение. В качестве альтернативы, вы можете позвонитьoutcome.get_result()
и использовать стандартную try...except
обработку.
Так как же пройти тестовый проход? У вас есть 3 основных варианта:
- Вы можете пометить тест как ожидаемый сбой, позвонив
pytest.xfail()
оболочку.
- Вы можете пометить элемент как пропущенный , что означает, что тест никогда не запускался, вызвав
pytest.skip()
.
- Вы можете удалить исключение, используя
outcome.force_result()
метод ; установить результат в пустой список здесь (то есть: зарегистрированный хук не дал ничего, кромеNone
), и исключение будет полностью очищено.
То, что вы используете, зависит от вас. Не забудьте сначала проверить результаты пропущенных тестов и тестов с ожидаемым отказом, поскольку вам не нужно обрабатывать эти случаи, как если бы тест не прошел. Вы можете получить доступ к специальным исключениям, которые эти опции вызывают черезpytest.skip.Exception
иpytest.xfail.Exception
.
Вот пример реализации, которая помечает неудачные тесты, которые не вызывают ValueError
, как пропущенные :
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
outcome = yield
try:
outcome.get_result()
except (pytest.xfail.Exception, pytest.skip.Exception, pytest.exit.Exception):
raise # already xfailed, skipped or explicit exit
except ValueError:
raise # not ignoring
except (pytest.fail.Exception, Exception):
# turn everything else into a skip
pytest.skip("[NOTRUN] ignoring everything but ValueError")
Когда положить в conftest.py
выходной становится:
$ pytest -r a demo/test_foo.py
============================= test session starts =============================
platform darwin -- Python 3.8.0, pytest-3.10.0, py-1.7.0, pluggy-0.8.0
rootdir: ..., inifile:
collected 2 items
demo/test_foo.py sF [100%]
=================================== FAILURES ===================================
__________________________________ test_spam ___________________________________
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
=========================== short test summary info ============================
FAIL demo/test_foo.py::test_spam
SKIP [1] .../demo/conftest.py:12: [NOTRUN] ignoring everything but ValueError
===================== 1 failed, 1 skipped in 0.07 seconds ======================
Я использовал -r a
флаг, чтобы прояснить, что test_ham
было пропущено сейчас.
Если вы замените pytest.skip()
вызов на pytest.xfail("[XFAIL] ignoring everything but ValueError")
, тест помечается как ожидаемый сбой:
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
и используя outcome.force_result([])
пометки как пройденные:
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
Вам решать, какой из них вы считаете наиболее подходящим для вашего варианта использования. Для skip()
и xfail()
я имитировал стандартный формат сообщения (с префиксом [NOTRUN]
или[XFAIL]
), но вы можете использовать любой другой формат сообщения, который вы хотите.
Во всех трех случаях pytest не будет открывать отладчик для тестов, результаты которых вы изменили с помощью этого метода.
Изменение отдельных утверждений
Если вы хотите изменить assert
тесты внутри теста , то вы настраиваете себя на гораздо большую работу. Да, это технически возможно, но только путем переписывания самого кода, который Python собирается выполнить во время компиляции .
Когда вы используете pytest
, это на самом деле уже делается . Pytest переписывает assert
утверждения, чтобы дать вам больше контекста, когда ваши утверждения не выполняются ; см. этот блог для хорошего обзора того, что именно делается, а также _pytest/assertion/rewrite.py
исходного кода . Обратите внимание, что этот модуль имеет длину более 1 тыс. Строк и требует понимания того, как работают абстрактные синтаксические деревья Python . Если вы делаете, вы могли бы monkeypatch этот модуль , чтобы добавить свои собственные модификации там, включая окружающие assert
сtry...except AssertionError:
обработчиком.
Однако вы не можете просто выборочно отключать или игнорировать утверждения, потому что последующие операторы могут легко зависеть от состояния (конкретные расположения объектов, набор переменных и т. Д.), От которого пропущенное утверждение предназначалось для защиты. Если проверка assert foo
не выполняется None
, то более позднее подтверждение полагается на foo.bar
существование, тогда вы просто столкнетесь сAttributeError
там и т. Д. Придерживайтесь повторного вызова исключения, если вам нужно пойти по этому пути.
Я не буду вдаваться в подробности переписывания asserts
здесь, так как не думаю, что это стоит того, чтобы продолжить, не учитывая объем работы и посмертную отладку, которая дает вам доступ к состоянию теста на в любом случае точка подтверждения отказа .
Обратите внимание, что если вы действительно хотите это сделать, вам не нужно использовать eval()
(это не сработает в любом случае, assert
это утверждение, поэтому вам нужно будет использовать exec()
вместо этого), и при этом вам не придется запускать утверждение дважды (что может привести к проблемам, если выражение, используемое в утверждении изменено состояние). Вместо этого вы бы встроили ast.Assert
узел вast.Try
узел и подключить обработчик исключений, который использует пустой ast.Raise
узел, повторно вызвать исключение, которое было перехвачено.
Использование отладчика для пропуска утверждений утверждения.
Отладчик Python фактически позволяет пропускать операторы , используя j
/ jump
команду . Если вы знаете , фронт , что конкретное утверждение будет терпеть неудачу, вы можете использовать это , чтобы обойти его. Вы можете запустить свои тесты с помощью --trace
, который открывает отладчик в начале каждого теста , а затем выполнить j <line after assert>
команду a, чтобы пропустить его, когда отладчик приостановлен непосредственно перед утверждением.
Вы даже можете автоматизировать это. Используя описанные выше приемы, вы можете создать собственный плагин отладчика, который
- использует
pytest_testrun_call()
ловушку, чтобы поймать AssertionError
исключение
- извлекает номер строки «ошибочной» строки из трассировки, и, возможно, при некотором анализе исходного кода определяет номера строк до и после утверждения, необходимого для выполнения успешного перехода
- снова запускает тест , но на этот раз с использованием
Pdb
подкласса, который устанавливает точку останова на строке перед подтверждением и автоматически выполняет переход к секунде при достижении точки останова с последующим c
продолжением.
Или вместо того, чтобы ждать, пока утверждение не сработает, вы можете автоматизировать установку точек останова для каждого assert
найденного в тесте (опять же, используя анализ исходного кода, вы можете тривиально извлечь номера строк для ast.Assert
узлов в AST теста), выполнить проверенный тест используя команды сценария отладчика, и используйте jump
команду, чтобы пропустить само утверждение. Вы должны были бы сделать компромисс; запускать все тесты в отладчике (что медленно, поскольку интерпретатору приходится вызывать функцию трассировки для каждого оператора) или применять его только к ошибочным тестам и платить цену за повторный запуск этих тестов с нуля.
Такой плагин было бы много работы, чтобы создать, я не собираюсь писать пример здесь, частично потому, что он не вписывается в ответ в любом случае, и частично, потому что я не думаю, что это стоит времени . Я бы просто открыл отладчик и сделал прыжок вручную. Неудачное утверждение указывает на ошибку либо в самом тесте, либо в тестируемом коде, так что вы можете просто сосредоточиться на устранении проблемы.