Ви використовуєте 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
(у який момент піст-тест після цього закінчується).
Ось приклад реалізації, який показує, що ми відмовляємось від налагодження та повертаємось негайно, якщо тест не піднятий 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()
.
Коли піст-тест виконує тести, він виконуватиме тест через вищезазначений гак, який, як очікується, поверне None
або підвищить виняток. З цього створюється звіт, необов'язково створюється запис журналу, і якщо тест не вдався, pytest_exception_interact()
викликається вищевказаний гак. Отже, все, що вам потрібно зробити, - це змінити результат, який дає цей гачок; замість винятку він просто не повинен взагалі нічого повертати.
Найкращий спосіб зробити це - використовувати обгортку для гаків . Обгортки з гаків не повинні виконувати фактичну роботу, а натомість надають можливість змінити те, що відбувається з результатом гачка. Все, що вам потрібно зробити - це додати рядок:
outcome = yield
у реалізації вашої обертової оболонки, і ви отримуєте доступ до результату гачка , включаючи тестовий виняток через outcome.excinfo
. Цей атрибут встановлюється в кордоні (тип, екземпляр, зворотний зв'язок), якщо в тесті було піднято виняток. Крім того, ви можете зателефонувати outcome.get_result()
та використовувати стандартне try...except
керування.
Тож як зробити пропускний тест? У вас є три основні варіанти:
- Ви можете позначити тест як очікуваний збій, зателефонувавши
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 . Якщо ви це зробите, ви можете виправити цей модуль, щоб додати там свої власні модифікації, включаючи оточення 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
виняток
- вилучає рядок "порушує" номер рядка з прослідковування, і, можливо, за допомогою аналізу вихідного коду визначає номери рядків до і після твердження, необхідного для виконання успішного стрибка
- запускає тест ще раз , але на цей раз використовуючи
Pdb
підклас, який встановлює точку розриву на лінії перед затвердженням і автоматично виконує стрибок на секунду при попаданні точки зламу з подальшим c
продовженням.
Або замість того, щоб чекати, коли твердження не вдасться, ви могли автоматизувати встановлення точок перерв для кожного assert
знайденого в тесті (знову ж таки, використовуючи аналіз вихідного коду, ви можете тривіально витягувати номери рядків для ast.Assert
вузлів в AST тесту), виконати затверджений тест використовуючи сценарії команд налагодження, і використовуйте jump
команду для пропуску самого твердження. Вам доведеться здійснити компроміс; запустити всі тести під налагоджувачем (це повільно, оскільки інтерпретатору доводиться викликати функцію відстеження для кожного оператора) або застосувати це лише до невдалих тестів і заплатити ціну повторного запуску цих тестів з нуля.
Такий плагін був би великою роботою для створення, я не збираюся писати приклад тут, частково тому, що він все одно не впишеться у відповідь, а частково тому, що я не думаю, що варто того часу . Я б просто відкрити налагоджувач і здійснити стрибок вручну. Невдале ствердження вказує на помилку як у самому тесті, так і під тестом коду, тому ви можете просто зосередитись на налагодженні проблеми.