Можно ли изменить поведение оператора assert в Python? - PullRequest
18 голосов
/ 10 октября 2019

Я использую операторы Python assert, чтобы соответствовать фактическому и ожидаемому поведению. У меня нет контроля над этим, как будто есть ошибки прерванные тестовые случаи. Я хочу взять на себя управление ошибкой утверждения и определить, хочу ли я прервать тестовый сценарий при ошибке подтверждения или нет.

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

Я понятия не имею, как это сделать

Пример кода, здесь мы используем pytest

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Below is my expectation

Когда утверждаемвыдает assertionError, у меня должна быть возможность приостановить тестирование, и я могу отлаживать и позже возобновлять. Для паузы и возобновления я буду использовать tkinter модуль. Я сделаю функцию подтверждения, как показано ниже

import tkinter
import tkinter.messagebox

top = tkinter.Tk()

def _assertCustom(assert_statement, pause_on_fail = 0):
    #assert_statement will be something like: assert a == 10, "Some error"
    #pause_on_fail will be derived from global file where I can change it on runtime
    if pause_on_fail == 1:
        try:
            eval(assert_statement)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            eval (assert_statement)
            #Above is to raise the assertion error again to fail the testcase
    else:
        eval (assert_statement)

В дальнейшем я должен изменить каждое утверждение assert с помощью этой функции следующим образом:

import pytest
def test_abc():
    a = 10
    # Suppose some code and below is the assert statement 
    _assertCustom("assert a == 10, 'error message'")

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

Summary: Мне нужно что-то, где я могу приостановить тестовый случай при сбое, а затем возобновить после отладки. Я знаю о tkinter, и именно поэтому я использовал его. Любые другие идеи будут приветствоваться

Note: Выше код еще не проверен. Также могут быть небольшие синтаксические ошибки

Редактировать: Спасибо за ответы. Расширяя этот вопрос немного впереди сейчас. Что делать, если я хочу изменить поведение assert. В настоящее время, когда есть утверждение об ошибке, тестовый случай завершается. Что делать, если я хочу выбрать, нужен ли мне выход из тестового набора при конкретном сбое подтверждения или нет. Я не хочу писать пользовательскую функцию assert, как упомянуто выше, потому что таким образом я должен изменить количество мест

Ответы [ 4 ]

23 голосов
/ 21 октября 2019

Вы используете 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, чтобы пропустить само утверждение. Вы должны были бы сделать компромисс;запускать все тесты в отладчике (что медленно, так как интерпретатору приходится вызывать функцию трассировки для каждого оператора) или применять это только к ошибочным тестам и платить цену за повторный запуск этих тестов с нуля.

Такой плагин было бы много работы, чтобы создать, я не собираюсь писать пример здесь, частично потому, что он не вписывается в ответ в любом случае, и частично, потому что Я неНе думаю, что это стоит времени . Я бы просто открыл отладчик и сделал прыжок вручную. Неудачное утверждение указывает на ошибку либо в самом тесте, либо в тестируемом коде, поэтому вы можете просто сосредоточиться на устранении проблемы.

7 голосов
/ 16 октября 2019

Вы можете достичь именно того, что вы хотите, без каких-либо изменений кода с помощью pytest --pdb .

На вашем примере:

import pytest
def test_abc():
    a = 9
    assert a == 10, "some error message"

Запускать с -pdb:

py.test --pdb
collected 1 item

test_abc.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_abc():
        a = 9
>       assert a == 10, "some error message"
E       AssertionError: some error message
E       assert 9 == 10

test_abc.py:4: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /private/tmp/a/test_abc.py(4)test_abc()
-> assert a == 10, "some error message"
(Pdb) p a
9
(Pdb)

Как только тест не пройден, вы можете отладить его с помощью встроенного отладчика python. Если вы закончили отладку, вы можете continue с остальными тестами.

4 голосов
/ 20 октября 2019

Одним из простых решений, если вы хотите использовать код Visual Studio, может быть использование условных точек останова .

Это позволит вам настроить свои утверждения, например:

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Затем добавьте условную точку останова в строку подтверждения, которая будет прерываться только при сбое утверждения:

enter image description here

4 голосов
/ 16 октября 2019

Если вы используете PyCharm, вы можете добавить точку прерывания исключения, чтобы приостановить выполнение при сбое подтверждения. Выберите Просмотр точек останова (CTRL-SHIFT-F8) и добавьте обработчик исключений при повышении для AssertionError. Обратите внимание, что это может замедлить выполнение тестов.

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

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

  2. Вы можете определить следующий декоратор и украсить им каждую соответствующую функцию тестирования. (Помимо регистрации сообщения, вы также можете запустить pdb.post_mortem в этой точке или даже интерактивный code.interact с локальными элементами кадра, где возникло исключение, какописано в этом ответе .)

from functools import wraps

def pause_on_assert(test_func):
    @wraps(test_func)
    def test_wrapper(*args, **kwargs):
        try:
            test_func(*args, **kwargs)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            # re-raise exception to make the test fail
            raise
    return test_wrapper

@pause_on_assert
def test_abc()
    a = 10
    assert a == 2, "some error message"

Если вы не хотите вручную декорировать каждую тестовую функцию, вы можете вместо этого определить автоматическое устройство, которое проверяет sys.last_value :
import sys

@pytest.fixture(scope="function", autouse=True)
def pause_on_assert():
    yield
    if hasattr(sys, 'last_value') and isinstance(sys.last_value, AssertionError):
        tkinter.messagebox.showinfo(sys.last_value)
...