Вы используете 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 отладка и взаимодействие hooks ;это точки подключения для настройки поведения, чтобы заменить или улучшить то, как 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()
для тестирования каждого плагина.
Сбойвообще исчезнуть
Хотя это дает вам полный контроль над отладкой неудачных тестов, это все равно оставляет тест как fail , даже если вы решили не открывать отладчик для данного теста. Если вы хотите, чтобы сбои полностью исчезли, вы можете использовать другой хук: pytest_runtest_call()
.
Когда pytest запускает тесты, он запускает тест через вышеуказанный хук,который, как ожидается, вернет None
или вызовет исключение. Исходя из этого создается отчет, при необходимости создается запись в журнале, и, если тест не пройден, вызывается вышеупомянутый хук pytest_exception_interact()
. Так что все, что вам нужно сделать, это изменить результат, полученный этим хуком;вместо исключения он вообще ничего не должен возвращать.
Лучший способ сделать это - использовать упаковщик крюка . Обертки с крючками не должны выполнять фактическую работу, но вместо этого им предоставляется возможность изменить то, что происходит с результатом крючка. Все, что вам нужно сделать, это добавить строку:
outcome = yield
в вашу реализацию обработчика перехвата, и вы получите доступ к результату перехвата , включая исключение теста через outcome.excinfo
. Этот атрибут имеет значение кортеж (тип, экземпляр, трассировка), если в тесте возникло исключение. Кроме того, вы можете позвонить outcome.get_result()
и использовать стандартную обработку try...except
.
Итак, как сделать неудачный тестовый проход? У вас есть 3 основных варианта:
- Вы можете пометить тест как ожидаемый сбой, вызвав
pytest.xfail()
в оболочке. - Вы можете пометить элемент как skipped , что делает вид, что тест никогда не выполнялся, вызвав
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
Если вы хотите изменить assert
тесты в рамках теста , затем вы настраиваетесь на гораздо большую работу. Да, это технически возможно, но только путем переписывания самого кода, который Python собирается выполнить во время компиляции .
Когда вы используете pytest
, этона самом деле уже делается . Pytest переписывает assert
операторы, чтобы дать вам больше контекста, когда ваши утверждения терпят неудачу ;см. в этом блоге для хорошего обзора того, что именно делается, а также _pytest/assertion/rewrite.py
исходный код . Обратите внимание, что этот модуль имеет длину более 1 тыс. Строк и требует, чтобы вы понимали, как абстрактные синтаксические деревья Python работают. Если вы это сделаете, вы сможете установить этот модуль, чтобы добавить свои собственные модификации, включая окружение assert
обработчиком try...except AssertionError:
.
Однако , выне может просто отключить или игнорировать утверждения выборочно, потому что последующие операторы могут легко зависеть от состояния (специфическое расположение объектов, набор переменных и т. д.), от которого пропущенное утверждение предназначалось для защиты. Если утверждение проверяет, что foo
не является None
, то более позднее утверждение полагается на foo.bar
, тогда вы просто столкнетесь с AttributeError
и т. Д. Придерживайтесь повторного вызова исключения, есливам нужно идти по этому пути.
Я не буду вдаваться в подробности переписывания asserts
здесь, так как не думаю, что это стоит того, чтобы продолжить, учитывая объем работы, ипосмертная отладка дает вам доступ к состоянию теста в момент ошибки подтверждения в любом случае .
Обратите внимание, что если вы действительно хотите это сделать, вам не нужноиспользуйте eval()
(который все равно не будет работать, assert
- это оператор, поэтому вам нужно будет использовать exec()
вместо этого), и при этом вам не придется запускать утверждение дважды (что может привести к проблемам, если выражениеиспользуется в утвержденном измененном состоянии). Вместо этого вы должны встроить узел ast.Assert
в узел ast.Try
и подключить обработчик исключений, который использует пустой узел ast.Raise
, чтобы повторно вызвать обнаруженное исключение.
Использование отладчика для пропуска утверждениязаявления.
Отладчик Python фактически позволяет пропускать операторы , используя j
/ jump
команду . Если вы знаете заранее , что конкретное утверждение потерпит неудачу , вы можете использовать это, чтобы обойти его. Вы можете запустить свои тесты с помощью --trace
, который открывает отладчик в начале каждого теста , а затем выполнить j <line after assert>
, чтобы пропустить его, когда отладчик приостановлен непосредственно перед утверждением.
Вы можете даже автоматизировать это. Используя описанные выше приемы, вы можете создать собственный плагин отладчика, который
- использует хук
pytest_testrun_call()
, чтобы поймать исключение AssertionError
- , которое извлекает номер строки "оскорбительной" строки изtraceback и, возможно, с помощью некоторого анализа исходного кода определяют номера строк до и после подтверждения, необходимого для успешного выполнения перехода
- запускает тест снова , но на этот раз с использованием подкласса
Pdb
который устанавливает точку останова на строке перед утверждением и автоматически выполняет переход ко второму при достижении точки останова, за которым следует c
continue.
Или вместо ожидания утверждениячтобы потерпеть неудачу, вы можете автоматизировать установку точек останова для каждого assert
, найденного в тесте (снова используя анализ исходного кода, вы можете тривиально извлечь номера строк для ast.Assert
узлов в AST теста), выполнить утвержденный тест с помощью отладчикаскриптовые команды и используйте команду jump
, чтобы пропустить само утверждение. Вы должны были бы сделать компромисс;запускать все тесты в отладчике (что медленно, так как интерпретатору приходится вызывать функцию трассировки для каждого оператора) или применять это только к ошибочным тестам и платить цену за повторный запуск этих тестов с нуля.
Такой плагин было бы много работы, чтобы создать, я не собираюсь писать пример здесь, частично потому, что он не вписывается в ответ в любом случае, и частично, потому что Я неНе думаю, что это стоит времени . Я бы просто открыл отладчик и сделал прыжок вручную. Неудачное утверждение указывает на ошибку либо в самом тесте, либо в тестируемом коде, поэтому вы можете просто сосредоточиться на устранении проблемы.