Skip to content

Document parametrizing conditional raises #4682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Anthony Shaw
Anthony Sottile
Anton Lodder
Antony Lee
Arel Cordero
Armin Rigo
Aron Coyle
Aron Curzon
Expand Down
1 change: 1 addition & 0 deletions changelog/4324.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises.
47 changes: 47 additions & 0 deletions doc/en/example/parametrize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,50 @@ As the result:
- The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing.
- The test ``test_eval[basic_2+4]`` passed.
- The test ``test_eval[basic_6*9]`` was expected to fail and did fail.

.. _`parametrizing_conditional_raising`:

Parametrizing conditional raising
--------------------------------------------------------------------

Use :func:`pytest.raises` with the
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
in which some tests raise exceptions and others do not.

It is helpful to define a no-op context manager ``does_not_raise`` to serve
as a complement to ``raises``. For example::

from contextlib import contextmanager
import pytest

@contextmanager
def does_not_raise():
yield


@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

In the example above, the first three test cases should run unexceptionally,
while the fourth should raise ``ZeroDivisionError``.

If you're only supporting Python 3.7+, you can simply use ``nullcontext``
to define ``does_not_raise``::

from contextlib import nullcontext as does_not_raise

Or, if you're supporting Python 3.3+ you can use::

from contextlib import ExitStack as does_not_raise

Or, if desired, you can ``pip install contextlib2`` and use::

from contextlib2 import ExitStack as does_not_raise
8 changes: 8 additions & 0 deletions src/_pytest/python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,14 @@ def raises(expected_exception, *args, **kwargs):
...
>>> assert exc_info.type is ValueError

**Using with** ``pytest.mark.parametrize``

When using :ref:`pytest.mark.parametrize ref`
it is possible to parametrize tests such that
some runs raise an exception and others do not.

See :ref:`parametrizing_conditional_raising` for an example.

**Legacy form**

It is possible to specify a callable by passing a to-be-called lambda::
Expand Down
48 changes: 48 additions & 0 deletions testing/python/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,54 @@ def test_raise_wrong_exception_passes_by():
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*3 passed*"])

def test_does_not_raise(self, testdir):
testdir.makepyfile(
"""
from contextlib import contextmanager
import pytest

@contextmanager
def does_not_raise():
yield

@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
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*4 passed*"])

def test_does_not_raise_does_raise(self, testdir):
testdir.makepyfile(
"""
from contextlib import contextmanager
import pytest

@contextmanager
def does_not_raise():
yield

@pytest.mark.parametrize('example_input,expectation', [
(0, does_not_raise()),
(1, pytest.raises(ZeroDivisionError)),
])
def test_division(example_input, expectation):
'''Test how much I know division.'''
with expectation:
assert (6 / example_input) is not None
"""
)
result = testdir.runpytest()
result.stdout.fnmatch_lines(["*2 failed*"])

def test_noclass(self):
with pytest.raises(TypeError):
pytest.raises("wrong", lambda: None)
Expand Down