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; assert
aumenta, AssertionError
mas 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...except
bloco, 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 --pdb
opçã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 q
comando quit, o pytest também encerra a execução, usando c
for continue retornará o controle para pytest e o próximo teste será executado.
Usando um depurador alternativo
Você não está vinculado ao pdb
depurador para isso; você pode definir um depurador diferente com a --pdbcls
opçã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 -s
switch seja usado ou um plug-in especial ). O switch utiliza um módulo e uma classe, por exemplo, para usar, pudb
você pode usar:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Você pode usar esse recurso para escrever sua própria classe wrapper para Pdb
que simplesmente retorna imediatamente se o fracasso específico não é algo que você está interessado. pytest
Usos 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, pytest
continua 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 ValueError
salvo, 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_type
para 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 TerminalReporter
plugin 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_interact
gancho 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 pdbinvoke
plug-in interno . Quando o gancho é chamado, o exemplo é testado em relação ao call.exceptinfo
objeto ; você também pode verificar o nó ou o relatório .
Com o código de exemplo acima no lugar demo/conftest.py
, a test_ham
falha de teste é ignorada, apenas a test_spam
falha 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 node
argumento) e acesso direto à exceção gerada (por meio da call.excinfo
ExceptionInfo
instância).
Observe que plugins específicos para depurador pytest (como pytest-pudb
ou pytest-pycharm
) registram seus próprios pytest_exception_interact
hooksp. 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_plugin
e 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 None
ou 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...except
manuseio 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.Exception
e 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.py
saí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 a
bandeira para deixar mais claro o que test_ham
foi 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 assert
testes 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 assert
instruçõ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.py
có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 assert
com 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 foo
não None
, então uma afirmação posterior depende da foo.bar
existência, então você simplesmente se deparará com uma AttributeError
aí, etc. Continue repetindo a exceção, se precisar seguir esse caminho.
Não vou entrar em mais detalhes sobre a reescrita asserts
aqui, 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.Assert
nó dentro de um ast.Try
nó e anexar um manipulador de exceção que usa um ast.Raise
nó 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 AssertionError
exceçã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
Pdb
subclasse 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 c
continuaçã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 assert
encontrado em um teste (novamente usando a análise do código-fonte, você pode extrair trivialmente números de linhas de ast.Assert
nós em um AST do teste), executar o teste declarado usando comandos com script do depurador e use o jump
comando 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.