Como afirmar corretamente que uma exceção é gerada no pytest?

293

Código:

# coding=utf-8
import pytest


def whatever():
    return 9/0

def test_whatever():
    try:
        whatever()
    except ZeroDivisionError as exc:
        pytest.fail(exc, pytrace=True)

Resultado:

================================ test session starts =================================
platform linux2 -- Python 2.7.3 -- py-1.4.20 -- pytest-2.5.2
plugins: django, cov
collected 1 items 

pytest_test.py F

====================================== FAILURES ======================================
___________________________________ test_whatever ____________________________________

    def test_whatever():
        try:
            whatever()
        except ZeroDivisionError as exc:
>           pytest.fail(exc, pytrace=True)
E           Failed: integer division or modulo by zero

pytest_test.py:12: Failed
============================== 1 failed in 1.16 seconds ==============================

Como fazer o rastreio de impressão pytest, para ver onde na whateverfunção uma exceção foi gerada?

Gill Bates
fonte
Eu recebo todo o rastreamento, o Ubuntu 14.04, Python 2.7.6
thefourtheye
@thefourtheye Faça o essencial com a saída, por favor. Eu tentei com Python 2.7.4 e Ubunthu 14.04 - com o mesmo resultado que eu descrevi no post principal.
Gill Bates
1
@GillBates você pode marcar a resposta correta?
Gonzalo Garcia

Respostas:

331

pytest.raises(Exception) é o que você precisa.

Código

import pytest

def test_passes():
    with pytest.raises(Exception) as e_info:
        x = 1 / 0

def test_passes_without_info():
    with pytest.raises(Exception):
        x = 1 / 0

def test_fails():
    with pytest.raises(Exception) as e_info:
        x = 1 / 1

def test_fails_without_info():
    with pytest.raises(Exception):
        x = 1 / 1

# Don't do this. Assertions are caught as exceptions.
def test_passes_but_should_not():
    try:
        x = 1 / 1
        assert False
    except Exception:
        assert True

# Even if the appropriate exception is caught, it is bad style,
# because the test result is less informative
# than it would be with pytest.raises(e)
# (it just says pass or fail.)

def test_passes_but_bad_style():
    try:
        x = 1 / 0
        assert False
    except ZeroDivisionError:
        assert True

def test_fails_but_bad_style():
    try:
        x = 1 / 1
        assert False
    except ZeroDivisionError:
        assert True

Resultado

============================================================================================= test session starts ==============================================================================================
platform linux2 -- Python 2.7.6 -- py-1.4.26 -- pytest-2.6.4
collected 7 items 

test.py ..FF..F

=================================================================================================== FAILURES ===================================================================================================
__________________________________________________________________________________________________ test_fails __________________________________________________________________________________________________

    def test_fails():
        with pytest.raises(Exception) as e_info:
>           x = 1 / 1
E           Failed: DID NOT RAISE

test.py:13: Failed
___________________________________________________________________________________________ test_fails_without_info ____________________________________________________________________________________________

    def test_fails_without_info():
        with pytest.raises(Exception):
>           x = 1 / 1
E           Failed: DID NOT RAISE

test.py:17: Failed
___________________________________________________________________________________________ test_fails_but_bad_style ___________________________________________________________________________________________

    def test_fails_but_bad_style():
        try:
            x = 1 / 1
>           assert False
E           assert False

test.py:43: AssertionError
====================================================================================== 3 failed, 4 passed in 0.02 seconds ======================================================================================

Observe que e_infosalva o objeto de exceção para que você possa extrair detalhes dele. Por exemplo, se você deseja verificar a pilha de chamadas de exceção ou outra exceção aninhada dentro.

Murilo Giacometti
fonte
34
Seria bom se você pudesse incluir um exemplo realmente consultando e_info. Para desenvolvedores mais familiarizados com outros idiomas, não é óbvio que o escopo e_infose estenda para fora do withbloco.
CJS
152

Você quer dizer algo como isto:

def test_raises():
    with pytest.raises(Exception) as execinfo:   
        raise Exception('some info')
    # these asserts are identical; you can use either one   
    assert execinfo.value.args[0] == 'some info'
    assert str(execinfo.value) == 'some info'
simpleranchero
fonte
16
excinfo.value.messagenão funcionou para mim, tive que usar str(excinfo.value), adicionou uma nova resposta #
224
5
@d_j, assert excinfo.value.args[0] == 'some info'é a forma direta de acesso a mensagem
maxschlepzig
assert excinfo.match(r"^some info$")funciona bem
Robin Nemeth 15/18
14
Desde a versão, 3.1você pode usar o argumento keyword matchpara afirmar que a exceção corresponde a um texto ou regex: with raises(ValueError, match='must be 0 or None'): raise ValueError("value must be 0 or None")ouwith raises(ValueError, match=r'must be \d+$'): raise ValueError("value must be 42")
Ilya Rusin
58

Existem duas maneiras de lidar com esse tipo de caso no pytest:

  • Usando a pytest.raisesfunção

  • Usando pytest.mark.xfaildecorador

Uso de pytest.raises:

def whatever():
    return 9/0
def test_whatever():
    with pytest.raises(ZeroDivisionError):
        whatever()

Uso de pytest.mark.xfail:

@pytest.mark.xfail(raises=ZeroDivisionError)
def test_whatever():
    whatever()

Saída de pytest.raises:

============================= test session starts ============================
platform linux2 -- Python 2.7.10, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- 
/usr/local/python_2.7_10/bin/python
cachedir: .cache
rootdir: /home/user, inifile:
collected 1 item

test_fun.py::test_whatever PASSED


======================== 1 passed in 0.01 seconds =============================

Saída do pytest.xfailmarcador:

============================= test session starts ============================
platform linux2 -- Python 2.7.10, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- 
/usr/local/python_2.7_10/bin/python
cachedir: .cache
rootdir: /home/user, inifile:
collected 1 item

test_fun.py::test_whatever xfail

======================== 1 xfailed in 0.03 seconds=============================

Como a documentação diz:

Usando pytest.raisesé provável que seja melhor para os casos em que você está testando exceções seu próprio código é deliberadamente aumentando, enquanto usando @pytest.mark.xfailcom uma função de verificação é provavelmente melhor para algo como documentar erros não corrigidos (onde o teste descreve o que “deve” acontecer) ou erros nas dependências .

veri_pudicha_coder
fonte
xfailnão é a solução para o problema aqui, apenas permite que o teste falhe. Aqui gostaríamos de verificar se uma certa exceção é gerada.
Ctrl-C
44

podes tentar

def test_exception():
    with pytest.raises(Exception) as excinfo:   
        function_that_raises_exception()   
    assert str(excinfo.value) == 'some info' 
d_j
fonte
Para obter a mensagem / valor da exceção como uma sequência no pytest 5.0.0, str(excinfo.value)é necessário usar . Também funciona no pytest 4.x. No pytest 4.x, str(excinfo)também funciona, mas não no pytest 5.0.0.
Makyen 29/06/19
Era isso que eu estava procurando. Obrigado
Eystein Bye
15

O pytest evolui constantemente e com uma das boas mudanças no passado recente agora é possível testar simultaneamente

  • o tipo de exceção (teste estrito)
  • a mensagem de erro (verificação rigorosa ou flexível usando uma expressão regular)

Dois exemplos da documentação:

with pytest.raises(ValueError, match='must be 0 or None'):
    raise ValueError('value must be 0 or None')
with pytest.raises(ValueError, match=r'must be \d+$'):
    raise ValueError('value must be 42')

Eu tenho usado essa abordagem em vários projetos e gosto muito dela.

Dr. Jan-Philip Gehrcke
fonte
6

O caminho certo está usando, pytest.raisesmas eu achei um caminho alternativo interessante nos comentários aqui e quero salvá-lo para futuros leitores desta pergunta:

try:
    thing_that_rasises_typeerror()
    assert False
except TypeError:
    assert True
Alexey Shrub
fonte
4

Esta solução é o que estamos usando:

def test_date_invalidformat():
    """
    Test if input incorrect data will raises ValueError exception
    """
    date = "06/21/2018 00:00:00"
    with pytest.raises(ValueError):
        app.func(date) #my function to be tested

Consulte pytest, https://docs.pytest.org/en/latest/reference.html#pytest-raises

SMDC
fonte
2

A melhor prática será usar uma classe que herda unittest.TestCase e executar self.assertRaises.

Por exemplo:

import unittest


def whatever():
    return 9/0


class TestWhatEver(unittest.TestCase):

    def test_whatever():
        with self.assertRaises(ZeroDivisionError):
            whatever()

Então você o executaria executando:

pytest -vs test_path
kerbelp
fonte
4
Melhor prática do que o que? Eu não chamaria o uso de sintaxe unittest, em vez da sintaxe pytest, "melhor prática".
Jean-François Corbett
2
Pode não ser 'melhor', mas é uma alternativa útil. Como o critério para uma resposta é utilidade, estou votando.
Reb.Cabin
parece que pytesté mais popular que nosex, mas é assim que eu uso o pytest.
Gang
0

Você já tentou remover "pytrace = True"?

pytest.fail(exc, pytrace=True) # before
pytest.fail(exc) # after

Você já tentou rodar com '--fulltrace'?

Loic Pantou
fonte