Is there pytest functionality similar to pytest.raises
that passes iff the block raises the specified exception, or doesn’t raise at all? Something like:
def test_encode_err(ulist): with pytest.maybe_raises_but_only(UnicodeEncodeError): # <== ? assert encode_list(ulist, 'ascii') == map(lambda x:x.encode('ascii'), ulist)
This question came up in the following situation..
The function to test:
def encode_list(lst, enc): "Encode all unicode values in ``lst`` using ``enc``." return [(x.encode(enc) if isinstance(x, unicode) else x) for x in lst]
A couple of simple tests (fixtures below):
def test_encode_err(ulist): with pytest.raises(UnicodeEncodeError): assert encode_list(ulist, 'ascii') def test_encode_u8(ulist, u8list): assert encode_list(ulist, 'u8') == u8list
The fixtures:
@pytest.fixture( scope='module', params=[ u'blåbærsyltetøy', u'', # <==== problem ] ) def ustr(request): print 'testing with:', `request.param` return request.param @pytest.fixture def u8str(ustr): return ustr.encode('u8') @pytest.fixture def ulist(ustr): return [ustr, ustr] @pytest.fixture def u8list(u8str): return [u8str, u8str]
the indicated <== problem
is only a problem for test_encode_err()
(and not test_encode_u8()
), and happens since u''.encode('ascii')
doesn’t raise a UnicodeEncodeError
(no unicode strings that doesn’t contain characters above code point 127 will raise).
Is there a py.test function that covers this use case?
Advertisement
Answer
I consider the provided response really incomplete. I like to parametrize tests for functions that could accepts different values.
Consider the following function that only accepts empty strings, in which case returns True
. If you pass other type raises a TypeError
and if the passed string is not empty a ValueError
.
def my_func_that_only_accepts_empty_strings(value): if isinstance(value, str): if value: raise ValueError(value) return True raise TypeError(value)
You can conveniently write parametric tests for all cases in a single test in different ways:
import contextlib import pytest parametrization = pytest.mark.parametrize( ('value', 'expected_result'), ( ('foo', ValueError), ('', True), (1, TypeError), (True, TypeError), ) ) @parametrization def test_branching(value, expected_result): if hasattr(expected_result, '__traceback__'): with pytest.raises(expected_result): my_func_that_only_accepts_empty_strings(value) else: assert my_func_that_only_accepts_empty_strings( value, ) == expected_result @parametrization def test_without_branching(value, expected_result): ctx = ( pytest.raises if hasattr(expected_result, '__traceback__') else contextlib.nullcontext ) with ctx(expected_result): assert my_func_that_only_accepts_empty_strings( value, ) == expected_result
Note that when an exception raises inside pytest.raises
context, the contexts exits so the later assert ... == expected_result
is not executed when the exception is catch. If other exception raises, it is propagated to your test so the comparison is not executed either. This allows you to write more assertions after the execution of the function for successfull calls.
But this can be improved in a convenient maybe_raises
fixture, that is what you’re looking for at first:
@contextlib.contextmanager def _maybe_raises(maybe_exception_class, *args, **kwargs): if hasattr(maybe_exception_class, '__traceback__'): with pytest.raises(maybe_exception_class, *args, **kwargs): yield else: yield @pytest.fixture() def maybe_raises(): return _maybe_raises
And the test can be rewritten as:
@parametrization def test_with_fixture(value, expected_result, maybe_raises): with maybe_raises(expected_result): assert my_func_that_only_accepts_empty_strings( value, ) == expected_result
Really nice, right? Of course you need to know how the magic works to write the test properly, always knowing that the context will exits when the exception is catched.
I think that pytest does not includes this because could be a really confusing pattern that could lead to unexpected false negatives and bad tests writing. Rather than that, pytest documentation encourauges you to pass expectation contexts as parameters but for me this solution looks really ugly.
EDIT: just packaged this fixture, see pytest-maybe-raises.