Reputation: 27361
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?
Upvotes: 2
Views: 457
Reputation: 484
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.
Upvotes: 1
Reputation: 95742
If you don't care when the exception is thrown just write the code as normal but put a try...except
block round it to ignore the error.
def test_encode_err(ulist):
try:
assert encode_list(ulist, 'ascii') == map(lambda x:x.encode('ascii'), ulist)
except UnicodeDecodeError:
pass
Really though consider whether you should be writing a test at all if you don't know whether the code will throw an exception. Try pinning down the data a bit more and having two tests, one which raises the exception and one which doesn't.
Upvotes: 4