Jak poprawnie stwierdzić, że wyjątek zostanie zgłoszony w pytest?

292

Kod:

# coding=utf-8
import pytest


def whatever():
    return 9/0

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

Wynik:

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

Jak wykonać śledzenie wydruku pytest, aby zobaczyć, gdzie w whateverfunkcji został zgłoszony wyjątek?

Gill Bates
źródło
Otrzymuję cały traceback, Ubuntu 14.04, Python 2.7.6
thefourtheye
@ thefourtheye Zrób gist z wyjściem, proszę. Próbowałem z Python 2.7.4 i Ubunthu 14.04 - z takim samym rezultatem, jak opisałem w głównym poście.
Gill Bates,
1
@GillBates może zaznaczyć prawidłową odpowiedź?
Gonzalo Garcia

Odpowiedzi:

330

pytest.raises(Exception) jest to, czego potrzebujesz.

Kod

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

Wynik

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

Pamiętaj, że e_infozapisuje obiekt wyjątku, aby można było z niego wyodrębnić szczegóły. Na przykład, jeśli chcesz sprawdzić stos wywołań wyjątków lub inny zagnieżdżony wyjątek w środku.

Murilo Giacometti
źródło
34
Byłoby dobrze, gdybyś mógł podać przykład zapytania e_info. Dla programistów bardziej zaznajomionych z niektórymi innymi językami, nie jest oczywiste, że zakres e_infowykracza poza withblok.
cjs
151

Czy masz na myśli coś takiego:

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'
prostotarz
źródło
16
excinfo.value.messagenie działało dla mnie, musiałem użyć str(excinfo.value), dodałem nową odpowiedź
d_j
5
@d_j, assert excinfo.value.args[0] == 'some info'to bezpośredni sposób dostępu do wiadomości
maxschlepzig
assert excinfo.match(r"^some info$")działa również
Robin Nemeth
14
Od wersji 3.1można użyć argumentu słowa kluczowego, matchaby potwierdzić, że wyjątek pasuje do tekstu lub wyrażenia regularnego: with raises(ValueError, match='must be 0 or None'): raise ValueError("value must be 0 or None")lubwith raises(ValueError, match=r'must be \d+$'): raise ValueError("value must be 42")
Ilya Rusin,
58

Istnieją dwa sposoby obsługi tego rodzaju przypadków w pytest:

  • Korzystanie z pytest.raisesfunkcji

  • Korzystanie z pytest.mark.xfaildekoratora

Zastosowanie pytest.raises:

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

Zastosowanie pytest.mark.xfail:

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

Wyjście 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 =============================

Wyjście pytest.xfailznacznika:

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

Jak mówi dokumentacja :

Używanie pytest.raisesjest prawdopodobnie lepsze w przypadkach, w których testujesz wyjątki, Twój własny kod celowo podnosi, podczas gdy używanie @pytest.mark.xfailz funkcją sprawdzania jest prawdopodobnie lepsze do czegoś takiego jak dokumentowanie nieusuniętych błędów (gdzie test opisuje, co powinno się stać) lub błędów w zależnościach .

veri_pudicha_coder
źródło
xfailnie jest tutaj rozwiązaniem problemu, pozwala jedynie na niepowodzenie testu. W tym miejscu chcielibyśmy sprawdzić, czy wystąpił jakiś wyjątek.
Ctrl-C
44

możesz spróbować

def test_exception():
    with pytest.raises(Exception) as excinfo:   
        function_that_raises_exception()   
    assert str(excinfo.value) == 'some info' 
d_j
źródło
Aby uzyskać komunikat / wartość wyjątku jako ciąg znaków w pytest 5.0.0, str(excinfo.value)wymagane jest użycie . Działa również w pytest 4.x. W pytest 4.x str(excinfo)również działa, ale nie działa w pytest 5.0.0.
Makyen
Właśnie tego szukałem. Dziękuję
Eystein Bye
15

pytest stale ewoluuje, a dzięki jednej z miłych zmian w niedawnej przeszłości można teraz jednocześnie testować

  • typ wyjątku (test ścisły)
  • komunikat o błędzie (ścisłe lub luźne sprawdzenie przy użyciu wyrażenia regularnego)

Dwa przykłady z dokumentacji:

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

Stosowałem to podejście w wielu projektach i bardzo mi się podobało.

Dr Jan-Philip Gehrcke
źródło
6

Właściwy sposób jest używany, pytest.raisesale znalazłem interesujący alternatywny sposób w komentarzach tutaj i chcę go zachować dla przyszłych czytelników tego pytania:

try:
    thing_that_rasises_typeerror()
    assert False
except TypeError:
    assert True
Alexey Shrub
źródło
2

Lepszą praktyką będzie korzystanie z klasy dziedziczącej unittest.TestCase i uruchamianie self.assertRaises.

Na przykład:

import unittest


def whatever():
    return 9/0


class TestWhatEver(unittest.TestCase):

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

Następnie wykonasz go, uruchamiając:

pytest -vs test_path
kerbelp
źródło
4
Lepsza praktyka niż co? W ogóle nie nazwałbym składni nieprzystosowanej zamiast pytest „lepsza praktyka”.
Jean-François Corbett
2
To może nie być „lepsze”, ale jest to przydatna alternatywa. Ponieważ kryterium odpowiedzi jest użyteczność, jestem zadowolony.
Reb.Cabin
wygląda na pytestto, że jest bardziej popularny niż nosex, ale w ten sposób używam pytest.
Gang
0

Czy próbowałeś usunąć „pytrace = True”?

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

Czy próbowałeś uruchomić z „--fulltrace”?

Loic Pantou
źródło