É possível alterar o comportamento da declaração de afirmação do PyTest em Python

18

Estou usando instruções de declaração Python para corresponder ao comportamento real e esperado. Eu não tenho controle sobre eles como se houvesse um caso de erro de abortamento. Desejo assumir o controle do erro de asserção e quero definir se quero interromper o testcase ao declarar falha ou não.

Também quero adicionar algo como, se houver um erro de asserção, o caso de teste deve ser pausado e o usuário pode retomar a qualquer momento.

Eu não tenho nenhuma idéia de como fazer isso

Exemplo de código, estamos usando pytest aqui

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

Below is my expectation

Quando assert lança um assertionError, eu deveria ter a opção de pausar o testcase e pode depurar e continuar mais tarde. Para pausar e retomar, usarei o tkintermódulo. Vou fazer uma função de afirmação como abaixo

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)

No futuro, tenho que alterar todas as declarações assert com esta função como

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

Isso é muito esforço para mim, pois tenho que fazer alterações em milhares de lugares onde já afirmei. Existe alguma maneira fácil de fazer isso empytest

Summary:Preciso de algo em que possa pausar o testcase em caso de falha e depois retomar após a depuração. Eu sei tkintere é por isso que eu o usei. Quaisquer outras idéias serão bem-vindas

Note: O código acima ainda não foi testado. Também pode haver pequenos erros de sintaxe

Edit: Obrigado pelas respostas. Estendendo esta questão um pouco à frente agora. E se eu quiser mudar o comportamento de afirmar. Atualmente, quando há um erro de asserção, o testcase sai. E se eu quiser escolher se preciso da saída do testcase em uma falha de declaração específica ou não. Eu não quero escrever a função de afirmação personalizada, como mencionado acima, porque dessa maneira eu tenho que mudar em vários lugares

Nitesh
fonte
3
Você poderia nos dar um exemplo de código do que gostaria de fazer?
Mrblewog 10/10/19
11
Não use, assertmas escreva suas próprias funções de verificação que fazem o que você deseja.
molbdnilo
Por que você não insere assert no bloco try e a mensagem de erro no exceto ?
Prathik Kini 10/10/19
11
Parece que o que você realmente deseja é usar pytestpara seus casos de teste. Ele suporta o uso de testes de afirmação e pulo, além de muitos outros recursos que facilitam a gravação de conjuntos de testes.
blubberdiblub
11
Não seria bem simples escrever uma ferramenta simples que substituísse mecanicamente todos os assert cond, "msg"códigos _assertCustom("assert cond, 'msg'")? Provavelmente uma sedfila poderia fazê-lo.
NPE

Respostas:

23

Você está usando pytest, o que oferece amplas opções para interagir com os testes que falharam. Ele fornece opções de linha de comando e vários ganchos para tornar isso possível. Vou explicar como usar cada um e onde você pode fazer personalizações para atender às suas necessidades específicas de depuração.

Também abordarei opções mais exóticas que permitiriam ignorar afirmações específicas por completo, se você realmente sentir que deve.

Manipular exceções, não afirmar

Observe que um teste com falha normalmente não para o pytest; somente se você ativou a solicitação explícita de sair após um certo número de falhas . Além disso, os testes falham porque uma exceção é gerada; assertaumenta, AssertionErrormas essa não é a única exceção que fará com que um teste falhe! Você deseja controlar como as exceções são tratadas, não as alteradas assert.

No entanto, uma afirmam não vai terminar o teste individual. Isso ocorre porque, quando uma exceção é gerada fora de um try...exceptbloco, o Python desenrola o quadro de função atual e não há como voltar atrás.

Não acho que é isso que você deseja, a julgar pela descrição de suas _assertCustom()tentativas de refazer a afirmação, mas discutirei suas opções ainda mais adiante.

Depuração post-mortem no pytest com pdb

Para as várias opções para lidar com falhas em um depurador, começarei com a --pdbopção de linha de comando , que abre o prompt de depuração padrão quando um teste falha (a saída é mais breve):

$ 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
[ ... ]

Com essa opção, quando um teste falha, o pytest inicia uma sessão de depuração post-mortem . Isto é essencialmente exatamente o que você queria; para interromper o código no ponto de um teste com falha e abrir o depurador para verificar o estado do seu teste. Você pode interagir com as variáveis ​​locais do teste, os globais e os locais e globais de todos os quadros da pilha.

Aqui, o pytest fornece controle total sobre a saída ou não após esse ponto: se você usar o qcomando quit, o pytest também encerra a execução, usando cfor continue retornará o controle para pytest e o próximo teste será executado.

Usando um depurador alternativo

Você não está vinculado ao pdbdepurador para isso; você pode definir um depurador diferente com a --pdbclsopção Qualquer implementação pdb.Pdb()compatível funcionaria, incluindo a implementação do depurador IPython ou a maioria dos outros depuradores Python (o depurador pudb exige que o -sswitch seja usado ou um plug-in especial ). O switch utiliza um módulo e uma classe, por exemplo, para usar, pudbvocê pode usar:

$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger

Você pode usar esse recurso para escrever sua própria classe wrapper para Pdbque simplesmente retorna imediatamente se o fracasso específico não é algo que você está interessado. pytestUsos Pdb()exatamente como pdb.post_mortem()faz :

p = Pdb()
p.reset()
p.interaction(None, t)

Aqui, té um objeto de rastreamento . Quando p.interaction(None, t)retorna, pytestcontinua com o próximo teste, a menos que p.quitting esteja definido como True(nesse ponto, o pytest sai).

Aqui está um exemplo de implementação que mostra que estamos recusando a depuração e retorna imediatamente, a menos que o teste tenha sido ValueErrorsalvo, salvo como 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)

Quando eu uso isso com a demonstração acima, esta é a saída (novamente, elidida por questões de brevidade):

$ 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)

As introspecções acima sys.last_typepara determinar se a falha é 'interessante'.

No entanto, eu realmente não posso recomendar esta opção, a menos que você queira escrever seu próprio depurador usando tkInter ou algo semelhante. Observe que essa é uma grande empresa.

Falhas na filtragem; escolha e escolha quando abrir o depurador

O próximo nível é os pytest de depuração e de interacção ganchos ; esses são pontos de gancho para personalizações de comportamento, para substituir ou aprimorar como o pytest normalmente lida com coisas como lidar com uma exceção ou entrar no depurador via pdb.set_trace()ou breakpoint()(Python 3.7 ou mais recente).

A implementação interna desse gancho também é responsável por imprimir o >>> entering PDB >>>banner acima; portanto, usar esse gancho para impedir a execução do depurador significa que você não verá essa saída. Você pode ter seu próprio gancho e delegá-lo ao gancho original quando uma falha de teste é 'interessante', para filtrar as falhas de teste independentemente do depurador que você está usando! Você pode acessar a implementação interna acessando-a pelo nome ; o plugin de gancho interno para isso é nomeado pdbinvoke. Para impedir que ele seja executado, é necessário cancelar o registro , mas salvar uma referência, podemos chamá-lo diretamente, conforme necessário.

Aqui está uma implementação de exemplo desse gancho; você pode colocar isso em qualquer um dos locais em que os plugins são carregados ; Coloquei em 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)

O plugin acima usa o TerminalReporterplugin interno para escrever linhas no terminal; isso torna a saída mais limpa ao usar o formato padrão de status de teste compacto e permite gravar coisas no terminal, mesmo com a captura de saída ativada.

O exemplo registra o objeto de plug-in com pytest_exception_interactgancho por outro gancho, pytest_configure()mas certificando-se de que seja executado com atraso suficiente (usando @pytest.hookimpl(trylast=True)) para poder cancelar o registro do pdbinvokeplug-in interno . Quando o gancho é chamado, o exemplo é testado em relação ao call.exceptinfoobjeto ; você também pode verificar o ou o relatório .

Com o código de exemplo acima no lugar demo/conftest.py, a test_hamfalha de teste é ignorada, apenas a test_spamfalha de teste, que aumenta ValueError, resulta na abertura do prompt de depuração:

$ 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) 

Para reiterar, a abordagem acima tem a vantagem adicional de poder combinar isso com qualquer depurador que funcione com o pytest , incluindo pudb, ou o depurador 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>

Ele também tem muito mais contexto sobre qual teste estava sendo executado (por meio do nodeargumento) e acesso direto à exceção gerada (por meio da call.excinfo ExceptionInfoinstância).

Observe que plugins específicos para depurador pytest (como pytest-pudbou pytest-pycharm) registram seus próprios pytest_exception_interacthooksp. Uma implementação mais completa teria que passar por todos os plugins no gerenciador de plugins para substituir plugins arbitrários, automaticamente, usando config.pluginmanager.list_name_plugine hasattr()para testar cada plug-in.

Fazendo as falhas desaparecerem completamente

Embora isso ofereça controle total sobre a depuração de teste com falha, isso ainda deixa o teste com falha, mesmo se você optou por não abrir o depurador para um determinado teste. Se você quiser fazer as falhas desaparecem por completo, você pode fazer uso de um gancho diferente: pytest_runtest_call().

Quando o pytest executa testes, ele é executado através do gancho acima, que deve retornar Noneou gerar uma exceção. A partir disso, um relatório é criado, opcionalmente, uma entrada de log é criada e, se o teste falhar, o pytest_exception_interact()gancho mencionado acima é chamado. Então, tudo que você precisa fazer é mudar o resultado que esse gancho produz; em vez de uma exceção, ele simplesmente não deve retornar nada.

A melhor maneira de fazer isso é usar um invólucro de gancho . Os invólucros de gancho não precisam fazer o trabalho real, mas têm a chance de alterar o que acontece com o resultado de um gancho. Tudo que você precisa fazer é adicionar a linha:

outcome = yield

na implementação do wrapper de gancho e você obtém acesso ao resultado do gancho , incluindo a exceção de teste via outcome.excinfo. Este atributo é definido como uma tupla de (tipo, instância, rastreamento) se uma exceção foi gerada no teste. Como alternativa, você pode ligar outcome.get_result()e usar o try...exceptmanuseio padrão .

Então, como você faz um teste reprovado? Você tem 3 opções básicas:

  • Você pode marcar o teste como uma falha esperada , chamando pytest.xfail()o wrapper.
  • Você pode marcar o item como ignorado , o que finge que o teste nunca foi executado em primeiro lugar, ligando pytest.skip().
  • Você pode remover a exceção, usando o outcome.force_result()método ; defina o resultado como uma lista vazia aqui (ou seja: o gancho registrado não produziu nada além de None) e a exceção é totalmente limpa.

O que você usa depende de você. Verifique o resultado dos testes ignorados e de falha esperada primeiro, pois você não precisa lidar com esses casos como se o teste falhasse. Você pode acessar as exceções especiais que essas opções geram via pytest.skip.Exceptione pytest.xfail.Exception.

Aqui está um exemplo de implementação que marca testes com falha que não são disparados ValueError, como ignorados :

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")

Quando colocado na conftest.pysaída se torna:

$ 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 ======================

Usei a -r abandeira para deixar mais claro o que test_hamfoi ignorado agora.

Se você substituir a pytest.skip()chamada por pytest.xfail("[XFAIL] ignoring everything but ValueError"), o teste será marcado como uma falha esperada:

[ ... ]
XFAIL demo/test_foo.py::test_ham
  reason: [XFAIL] ignoring everything but ValueError
[ ... ]

e usando as outcome.force_result([])marcas como passadas:

$ pytest -v demo/test_foo.py  # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED                                        [ 50%]

Depende de você qual deles você se encaixa melhor no seu caso de uso. Para skip()e xfail()eu imitamos o formato de mensagem padrão (prefixado com [NOTRUN]ou [XFAIL]), mas você pode usar qualquer outro formato de mensagem que desejar.

Nos três casos, o pytest não abrirá o depurador para testes cujo resultado você alterou usando esse método.

Alterando declarações de afirmação individuais

Se você deseja alterar os asserttestes dentro de um teste , está se preparando para muito mais trabalho. Sim, isso é tecnicamente possível, mas apenas reescrevendo o próprio código que o Python executará no momento da compilação .

Quando você usa pytest, isso já está sendo feito . Pytest reescreve assertinstruções para fornecer mais contexto quando suas declarações falham ; consulte esta postagem no blog para obter uma boa visão geral do que está sendo feito, bem como do _pytest/assertion/rewrite.pycódigo-fonte . Observe que esse módulo tem mais de 1k linhas de comprimento e requer que você entenda como as árvores de sintaxe abstrata do Python funcionam. Se você fizer isso, você poderia monkeypatch esse módulo para adicionar suas próprias modificações lá, incluindo em torno do assertcom um try...except AssertionError:manipulador.

No entanto , você não pode simplesmente desabilitar ou ignorar afirmações seletivamente, porque as instruções subsequentes podem depender facilmente do estado (arranjos de objetos específicos, conjunto de variáveis ​​etc.) aos quais uma afirmação ignorada deveria se proteger. Se uma afirmação testa isso foonão None, então uma afirmação posterior depende da foo.barexistência, então você simplesmente se deparará com uma AttributeErroraí, etc. Continue repetindo a exceção, se precisar seguir esse caminho.

Não vou entrar em mais detalhes sobre a reescrita assertsaqui, pois acho que não vale a pena prosseguir, sem a quantidade de trabalho envolvida e com a depuração post-mortem, dando a você acesso ao estado do teste no ponto de falha de afirmação de qualquer maneira .

Observe que, se você quiser fazer isso, não precisará usar eval()(o que não funcionaria de qualquer maneira, asserté uma instrução, portanto, você precisará usá-lo exec()), nem precisará executar a asserção duas vezes (que pode levar a problemas se a expressão usada no estado da asserção for alterada). Em vez disso, você deve incorporar o ast.Assertnó dentro de um ast.Trynó e anexar um manipulador de exceção que usa um ast.Raisenó vazio novamente a exceção capturada.

Usando o depurador para pular instruções de asserção.

O depurador Python, na verdade, permite pular instruções usando o comando j/jump . Se você sabe de antemão que uma afirmação específica irá falhar, você pode usar isso para ignorá-lo. Você pode executar seus testes com --trace, o que abre o depurador no início de cada teste e , em seguida, emite um j <line after assert>para ignorá-lo quando o depurador é pausado pouco antes da declaração.

Você pode até automatizar isso. Usando as técnicas acima, você pode criar um plug-in de depurador personalizado que

  • usa o pytest_testrun_call()gancho para capturar a AssertionErrorexceção
  • extrai o número da linha 'ofensiva' do traceback e, talvez, com alguma análise do código-fonte, determine os números de linha antes e depois da afirmação necessária para executar um salto bem-sucedido
  • executa o teste novamente , mas desta vez usando uma Pdbsubclasse que define um ponto de interrupção na linha antes da declaração e executa automaticamente um salto para o segundo quando o ponto de interrupção é atingido, seguido de uma ccontinuação.

Ou, em vez de esperar por uma afirmação falhar, você pode automatizar a definição de pontos de interrupção para cada um assertencontrado em um teste (novamente usando a análise do código-fonte, você pode extrair trivialmente números de linhas de ast.Assertnós em um AST do teste), executar o teste declarado usando comandos com script do depurador e use o jumpcomando para ignorar a própria afirmação. Você teria que fazer uma troca; execute todos os testes em um depurador (que é lento, pois o intérprete precisa chamar uma função de rastreamento para cada instrução) ou apenas aplique isso a testes com falha e pague o preço de reexecutá-los do zero.

Esse plug-in seria muito trabalhoso para criar, não vou escrever um exemplo aqui, em parte porque não caberia em uma resposta de qualquer maneira e em parte porque acho que não vale a pena o tempo . Eu apenas abria o depurador e fazia o salto manualmente. Uma declaração com falha indica um erro no próprio teste ou no código em teste; portanto, você também pode se concentrar apenas na depuração do problema.

Martijn Pieters
fonte
7

Você pode conseguir exatamente o que deseja, sem absolutamente nenhuma modificação de código com pytest --pdb .

Com o seu exemplo:

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

Execute com --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)

Assim que um teste falha, você pode depurá-lo com o depurador python embutido. Se você tiver concluído a depuração, poderá fazê-lo continuecom o restante dos testes.

gnvk
fonte
Isso será interrompido quando o caso de teste falhar ou a etapa do teste falhar.
Nitesh
Verifique os documentos vinculados: doc.pytest.org/en/latest/…
gnvk
Excelente ideia. Mas se usar --pdb, o testcase fará uma pausa a cada falha. i pode decidir em tempo de execução na qual a falha que eu quero fazer uma pausa no caso de teste
Nitesh
5

Se você estiver usando PyCharm, poderá adicionar um ponto de interrupção de exceção para pausar a execução sempre que uma declaração falhar. Selecione Exibir pontos de interrupção (CTRL-SHIFT-F8) e adicione um manipulador de exceção em aumento para AssertionError. Observe que isso pode retardar a execução dos testes.

Caso contrário, se você não se importar em fazer uma pausa no final de cada teste com falha (um pouco antes do erro), em vez de no momento em que a afirmação falhar, você terá algumas opções. Observe, no entanto, que, nesse ponto, vários códigos de limpeza, como o fechamento de arquivos que foram abertos no teste, já podem ter sido executados. As opções possíveis são:

  1. Você pode dizer ao pytest para colocá-lo no depurador por erros, usando a opção --pdb .

  2. Você pode definir o decorador a seguir e decorar cada função de teste relevante com ele. (Além de registrar uma mensagem, você também pode iniciar um pdb.post_mortem neste momento ou até mesmo um código interativo. Interagir com os locais do quadro em que a exceção se originou, conforme descrito nesta resposta .)

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"
  1. Se você não desejar decorar manualmente todas as funções de teste, poderá definir um dispositivo autouse que inspeciona 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)
Uri Granta
fonte
Gostei da resposta com os decoradores, mas ela não pode ser feita dinamicamente. Quero controlar dinamicamente quando quero pause_on_assert ou não. Existe alguma solução para isso?
Nitesh
Dinâmico de que maneira? Como em um único switch para ativar / desativar em qualquer lugar? Ou alguma maneira de controlá-lo para cada teste?
Uri Granta
Suponha que eu esteja executando alguns casos de teste. No meio, tive a necessidade de fazer uma pausa em caso de falha. Vou habilitar o switch. Mais tarde, em qualquer ponto do tempo, sinto que preciso desativar o comutador.
Nitesh
Seu decorador em resposta: 2 não vai funcionar para mim porque o meu caso de teste terá múltiplos afirma
Nitesh
Em relação a um 'switch', você pode atualizar a implementação de pause_on_assertpara ler de um arquivo para decidir se deseja pausar ou não.
Uri Granta
4

Uma solução simples, se você estiver disposto a usar o Visual Studio Code, poderá ser usar pontos de interrupção condicionais .

Isso permitiria configurar suas asserções, por exemplo:

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

Em seguida, adicione um ponto de interrupção condicional em sua linha de asserção que só será interrompida quando sua asserção falhar:

insira a descrição da imagem aqui

Nick Martin
fonte
@ Nitesh - Eu acho que essa solução resolve todos os seus problemas, você só quebra quando uma afirmação falha, pode depurar o código ali e então e pode continuar com os testes restantes depois ... Embora seja um pouco mais complicado de configurar inicialmente
Nick Martin