Skip to content
Advertisement

py.test assert may raise, and if it raises it will be __

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.

Advertisement