Как использовать pytest, чтобы убедиться, что ошибка НЕ ​​возникает

Предположим, у нас есть что-то вроде этого:

import py, pytest

ERROR1 = ' --- Error : value < 5! ---'
ERROR2 = ' --- Error : value > 10! ---'

class MyError(Exception):
    def __init__(self, m):
        self.m = m

    def __str__(self):
        return self.m

def foo(i):
    if i < 5:
        raise MyError(ERROR1)
    elif i > 10:
        raise MyError(ERROR2)
    return i


# ---------------------- TESTS -------------------------
def test_foo1():
    with pytest.raises(MyError) as e:
        foo(3)
    assert ERROR1 in str(e)

def test_foo2():
    with pytest.raises(MyError) as e:
        foo(11)
    assert ERROR2 in str(e)

def test_foo3():
        ....
        foo(7)
         ....

Q: Как я могу заставить test_foo3 () проверять, что MyError не возникает? Очевидно, что я мог просто протестировать:

def test_foo3():
    assert foo(7) == 7

но я хочу проверить это через pytest.raises (). Возможно ли как-нибудь? Например: в случае, когда функция "foo" вообще не имеет возвращаемого значения,

def foo(i):
    if i < 5:
        raise MyError(ERROR1)
    elif i > 10:
        raise MyError(ERROR2)

имеет смысл протестировать таким образом, imho.


person paraklet    schedule 28.11.2013    source источник
comment
Похоже на поиск проблемы, проверка кода foo(7) в порядке. Вы получите правильное сообщение, и вам будет легче отлаживать весь вывод pytest. Предложение, которое вы выдвинули из @Faruk ('Unexpected error...'), ничего не говорит об ошибке, и вы застрянете. Единственное, что вы можете сделать, чтобы улучшить его, - это заявить о своем намерении, например, test_foo3_works_on_integers_within_range().   -  person dhill    schedule 11.12.2013
comment
По теме: Python unittest - противоположность assertRaises?   -  person Piotr Dobrogost    schedule 22.02.2021


Ответы (4)


Тест завершится неудачно, если возникнет непредвиденное исключение. Вы можете просто вызвать foo (7), и вы убедитесь, что MyError не возникает. Итак, достаточно будет следующего:

def test_foo3():
    foo(7)

Если вы хотите быть явным и написать для этого утверждение assert, вы можете сделать:

def test_foo3():
    try:
        foo(7)
    except MyError:
        pytest.fail("Unexpected MyError ..")
person Faruk Sahin    schedule 28.11.2013
comment
Спасибо, работает, но, похоже, это скорее взлом, чем чистое решение. Например, проверка для foo (4) завершится неудачно, но не из-за ошибки утверждения. - person paraklet; 29.11.2013
comment
проверка для foo (4) завершится ошибкой, потому что вызовет исключение, которого не ожидалось. Еще один способ - заключить его в блок try catch и дать сбой с определенным сообщением. Я обновлю свой ответ. - person Faruk Sahin; 29.11.2013
comment
Если у вас много таких случаев, может быть полезно написать это в простой функции: `` def not_raises (error_class, func, * args, ** kwargs): ... `` Или вы можете написать подобный подходу, как это делает pytest. Если вы это сделаете, я предлагаю вам написать пиар с этим, чтобы принести пользу всем. :) (Репозиторий находится в bitbucket). - person Bruno Oliveira; 01.12.2013
comment
@paraklet - основной слоган pytest - это тестирование без шаблонов. В духе pytest есть возможность писать тесты, как в первом примере Фарука, в то время как pytest обрабатывает детали за вас. Для меня первый пример - это чистое решение, а второй кажется излишне многословным. - person Nick Chammas; 19.12.2015
comment
Мне нравится, когда код читается. Если я вижу pytest.notRaises(), я ясно вижу, что цель теста - убедиться, что исключение не генерируется. Если я просто выполню код, а assert не последует, моя первая мысль, что здесь чего-то не хватает ... Да, я мог бы написать для этого комментарий, но я предпочитаю, чтобы код был понятным, а не комментариями. - person xavier; 24.02.2021

Основываясь на том, что упомянул Ойсин ..

Вы можете создать простую not_raises функцию, которая действует аналогично raises в pytest:

from contextlib import contextmanager

@contextmanager
def not_raises(exception):
  try:
    yield
  except exception:
    raise pytest.fail("DID RAISE {0}".format(exception))

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

person Pithikos    schedule 19.02.2017
comment
Я бы хотел, чтобы это было встроено в py.test; в некоторых случаях это сделало бы тесты более удобочитаемыми. Особенно в сочетании с @pytest.mark.parametrize. - person Arel; 22.08.2017
comment
Я высоко ценю удобочитаемость кода при таком подходе! - person GrazingScientist; 13.11.2020

Поскольку на этот вопрос был дан ответ, документы pytest обновили информацию по этому вопросу, о которой стоит упомянуть здесь.

https://docs.pytest.org/en/6.2.x/example/parametrize.html#parametrizing-conditional-raising.

Это похоже на некоторые другие ответы, но с использованием parametrize и более новой встроенной nullcontext, что делает решение действительно чистым.

Возможный пример Python3.7 + только будет выглядеть так:

from contextlib import nullcontext as does_not_raise
import pytest


@pytest.mark.parametrize(
    "example_input,expectation",
    [
        (3, does_not_raise()),
        (2, does_not_raise()),
        (1, does_not_raise()),
        (0, pytest.raises(ZeroDivisionError)),
    ],
)
def test_division(example_input, expectation):
    """Test how much I know division."""
    with expectation:
        assert (6 / example_input) is not None

Использование parametrize таким образом позволяет комбинировать контрольные примеры OP, например:

@pytest.mark.parametrize(
    "example_input,expectation,message",
    [
        (3, pytest.raises(MyError), ERROR1),
        (11, pytest.raises(MyError), ERROR2),
        (7, does_not_raise(), None),
    ],
)
def test_foo(example_input, expectation, message):
    with expectation as e:
        foo(example_input)
    assert message is None or message in str(e)

Таким образом вы можете проверить, не вызывало никаких исключений. nullcontext предназначен для замены дополнительного диспетчера контекста. (pytest.raises, в данном случае). На самом деле он ничего не делает, поэтому, если вы хотите проверить, НЕ вызывает конкретного исключения, вы должны увидеть один из других ответов.

person NikT    schedule 17.06.2021
comment
Это, в частности, описывает, как вы можете поменять pytest.raises на pytest.parametrize; на самом деле это не ответ на общий вопрос. Все, что дает вам nullcontext, это то, что работает с оператором with, но ничего не делает само по себе. - person Martijn Pieters; 04.07.2021
comment
@MartijnPieters подразумевается, что вы можете ответить на общий вопрос, используя его следующим образом: with does_not_raise(): foo(7). На мой взгляд, это предотвращает паузу, возникающую при чтении теста без ожидания, такого как голый foo(7). - person EliadL; 04.07.2021
comment
@MartijnPieters, когда я увидел исходные 3 теста в вопросе, мне показалось, что имеет смысл использовать parametrize для - о чем я должен был упомянуть. Я внес некоторые правки, чтобы связать это с вопросом. - person NikT; 05.07.2021

Мне было любопытно посмотреть, сработает ли not_raises. Быстрый тест (test_notraises.py):

from contextlib import contextmanager

@contextmanager
def not_raises(ExpectedException):
    try:
        yield

    except ExpectedException, err:
        raise AssertionError(
            "Did raise exception {0} when it should not!".format(
                repr(ExpectedException)
            )
        )

    except Exception, err:
        raise AssertionError(
            "An unexpected exception {0} raised.".format(repr(err))
        )

def good_func():
    print "hello"


def bad_func():
    raise ValueError("BOOM!")


def ugly_func():
    raise IndexError("UNEXPECTED BOOM!")


def test_ok():
    with not_raises(ValueError):
        good_func()


def test_bad():
    with not_raises(ValueError):
        bad_func()


def test_ugly():
    with not_raises(ValueError):
        ugly_func()

Кажется, это работает. Однако я не уверен, действительно ли он хорошо читается в тесте.

person Oisin    schedule 17.02.2016
comment
Обновлено для python3 gist.github.com/oisinmulvihill/45c14271fad771694e - person Oisin; 04.08.2019
comment
Хотя это добавляет к тесту удобочитаемое ожидание, на мой взгляд, «try-except» не дает дополнительной ценности, поскольку тест по-прежнему будет ОШИБКА, а не ОТКАЗ, если возникнет это исключение. Кажется, лучше использовать nullcontext или pytest.fail, как в других ответах. - person EliadL; 04.07.2021