Jak skorzystać z pytest, aby sprawdzić, czy błąd NIE jest zgłaszany

86

Załóżmy, że mamy coś takiego:

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

P: Jak mogę sprawić, by test_foo3 () sprawdzał, czy nie pojawia się MyError? To oczywiste, że mógłbym po prostu przetestować:

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

ale chcę to przetestować poprzez pytest.raises (). Czy jest to możliwe? Na przykład: w przypadku funkcja „foo” w ogóle nie ma wartości zwracanej,

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

może mieć sens testowanie w ten sposób, imho.

paraklet
źródło
Wygląda na to, że szukam problemu, testowanie kodu foo(7)jest w porządku. Otrzymasz odpowiednią wiadomość i łatwiej będzie debugować wszystkie dane wyjściowe pytest. Sugestia, którą wymusiłeś z @Faruk ( 'Unexpected error...') nie mówi nic o błędzie i utkniesz. Jedyne, co możesz zrobić, aby to poprawić, to wyrazić swój zamiar test_foo3_works_on_integers_within_range().
dhill

Odpowiedzi:

129

Test zakończy się niepowodzeniem, jeśli wystąpi jakikolwiek nieoczekiwany wyjątek. Możesz po prostu wywołać foo (7) i sprawdzisz, czy nie pojawia się MyError. Tak więc wystarczy:

def test_foo3():
    foo(7)

Jeśli chcesz być wyraźny i napisać w tym celu oświadczenie asertujące, możesz zrobić:

def test_foo3():
    try:
        foo(7)
    except MyError:
        pytest.fail("Unexpected MyError ..")
Faruk Sahin
źródło
3
Dzięki, to działa, ale wydaje się być bardziej hackem niż czystym rozwiązaniem. Na przykład test dla foo (4) zakończy się niepowodzeniem, ale nie z powodu błędu asercji.
paraklet
test dla foo (4) zakończy się niepowodzeniem, ponieważ zgłosi wyjątek, którego nie oczekiwano. Innym sposobem byłoby zawinięcie go w blok try catch i niepowodzenie z określoną wiadomością. Zaktualizuję odpowiedź.
Faruk Sahin
1
Jeśli masz wiele takich przypadków, warto napisać to w prostej funkcji: `` def not_raises (error_class, func, * args, ** kwargs): ... `` Lub możesz napisać podejście podobne jak pytest. Jeśli tak, proponuję napisać PR z tym, aby wszyscy skorzystali. :) (Repozytorium znajduje się w bitbucket ).
Bruno Oliveira
6
@paraklet - głównym sloganem pytesta jest „testowanie bez schematu” . Jest bardzo w duchu pytest, aby umożliwić Ci pisanie testów, jak w pierwszym przykładzie Faruka, podczas gdy pytest zajmuje się szczegółami za Ciebie. Dla mnie pierwszy przykład to „czyste rozwiązanie”, a drugi wydaje mi się niepotrzebnie rozwlekły.
Nick Chammas
22

Opierając się na tym, o czym wspomniał Oisin.

Możesz stworzyć prostą not_raisesfunkcję działającą podobnie do pytest raises:

from contextlib import contextmanager

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

To jest w porządku, jeśli chcesz się trzymać raises mieć odpowiednik, a tym samym wyniki testów są bardziej czytelne. W istocie jednak nie potrzebujesz niczego poza uruchomieniem bloku kodu, który chcesz przetestować we własnym wierszu - pytest i tak zakończy się niepowodzeniem, gdy tylko ten blok zgłosi błąd.

Pithikos
źródło
1
Chciałbym, żeby było to wbudowane w py.test; w niektórych przypadkach sprawiłoby to, że testy byłyby znacznie bardziej czytelne. Szczególnie w połączeniu z @pytest.mark.parametrize.
Arel
Bardzo cenię sobie poczucie czytelności kodu w tym podejściu!
GrazingScientist
7

Byłem ciekawy, czy not_raises zadziała. Szybki test to (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()

Wydaje się, że to działa. Jednak nie jestem pewien, czy naprawdę dobrze się czyta w teście.

Oisin
źródło