Как правильно утверждать, что исключение возникает в pytest?


294

Код:

# coding=utf-8
import pytest


def whatever():
    return 9/0

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

Вывод:

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

Как сделать трассировку печати Pytest, чтобы я мог видеть, где в whateverфункции было вызвано исключение?


Я получаю полную трассировку, Ubuntu 14.04, Python 2.7.6
thefourtheye

@ thefourtheye Сделайте суть, пожалуйста. Я пытался с Python 2.7.4 и Ubunthu 14.04 - с тем же результатом, который я описал в основном посте.
Джилл Бейтс,

1
@GillBates можете пометить правильный ответ?
Гонсало Гарсия

Ответы:


333

pytest.raises(Exception) это то, что вам нужно.

Код

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

Вывод

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

Обратите внимание, что e_infoсохраняет объект исключения, чтобы вы могли извлечь из него детали. Например, если вы хотите проверить стек вызовов исключений или другое вложенное исключение внутри.


36
Было бы хорошо, если бы вы могли привести пример запроса e_info. Для разработчиков, более знакомых с некоторыми другими языками, не очевидно, что область действия e_infoвыходит за пределы withблока.
cjs

154

Вы имеете в виду что-то вроде этого:

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'

16
excinfo.value.messageне работал у меня, пришлось использовать str(excinfo.value), добавил новый ответ
d_j

5
@d_j, assert excinfo.value.args[0] == 'some info'это прямой доступ к сообщению
maxschlepzig

assert excinfo.match(r"^some info$")работает также
Робин Немет

14
Начиная с версии, 3.1вы можете использовать ключевое слово аргумент, matchчтобы утверждать, что исключение соответствует тексту или регулярному выражению: with raises(ValueError, match='must be 0 or None'): raise ValueError("value must be 0 or None")илиwith raises(ValueError, match=r'must be \d+$'): raise ValueError("value must be 42")
Илья Русин

58

Есть два способа обработки таких случаев в pytest:

  • Используя pytest.raisesфункцию

  • Использование pytest.mark.xfailдекоратора

Использование pytest.raises:

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

Использование pytest.mark.xfail:

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

Выход 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 =============================

Вывод pytest.xfailмаркера:

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

Как сказано в документации :

Использование pytest.raises, вероятно, будет лучше для случаев, когда вы тестируете исключения, которые ваш собственный код намеренно вызывает, в то время как использование @pytest.mark.xfailс функцией проверки, вероятно, лучше для чего-то вроде документирования нефиксированных ошибок (где тест описывает, что должно произойти) или ошибок в зависимостях. ,


xfailэто не решение проблемы здесь, это просто позволяет тесту провалиться. Здесь мы хотели бы проверить, возникает ли определенное исключение.
Ctrl-C

44

можешь попробовать

def test_exception():
    with pytest.raises(Exception) as excinfo:   
        function_that_raises_exception()   
    assert str(excinfo.value) == 'some info' 

Чтобы получить сообщение об исключении / значение в виде строки в pytest 5.0.0, str(excinfo.value)необходимо использовать. Это также работает в pytest 4.x. В pytest 4.x str(excinfo)также работает, но не работает в pytest 5.0.0.
Макьен

Это то, что я искал. Спасибо
Bye

17

Pytest постоянно развивается, и с одним из приятных изменений в недавнем прошлом теперь можно одновременно тестировать на

  • тип исключения (строгий тест)
  • сообщение об ошибке (строгая или произвольная проверка с использованием регулярного выражения)

Два примера из документации:

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

Я использовал этот подход в ряде проектов и мне очень нравится.


6

Правильный путь используется, pytest.raisesно я нашел интересный альтернативный путь в комментариях здесь и хочу сохранить его для будущих читателей этого вопроса:

try:
    thing_that_rasises_typeerror()
    assert False
except TypeError:
    assert True


2

Лучшей практикой будет использование класса, который наследует unittest.TestCase, и запуск self.assertRaises.

Например:

import unittest


def whatever():
    return 9/0


class TestWhatEver(unittest.TestCase):

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

Тогда вы бы выполнили это, запустив:

pytest -vs test_path

4
Лучше практиковать, чем что? Я бы не назвал использование синтаксиса unittest вместо синтаксиса pytest "лучшей практикой" вообще.
Жан-Франсуа Корбетт

2
Это не может быть «лучше», но это полезная альтернатива. Поскольку критерием ответа является полезность, я голосую.
Reb.Cabin

Похоже, pytestэто более популярно, чем nosex, но вот как я использую Pytest.
Банда

0

Вы пытались удалить "pytrace = True"?

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

Вы пытались запустить с '--fulltrace'?

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.