Skip to content

Parameterize conditional raises #4679

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

Closed
Closed
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 @@ -26,6 +26,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/1830.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A context manager ``does_not_raise`` is added to complement ``raises`` in parametrized tests with conditional raises.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we agreed that we were not going to add this? It encourages tests which are of bad form

#1830 (comment)

#4324 was a followup to #1830 that we document how to implement does_not_raises if you absolutely need it, not that we actually implement it.

I was and still am 👎 on this.

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.
25 changes: 25 additions & 0 deletions doc/en/example/parametrize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,28 @@ 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` and :func:`pytest.does_not_raise` together with the
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests in which some
tests raise exceptions and others do not. For example::

import pytest

@pytest.mark.parametrize('example_input,expectation', [
(3, pytest.does_not_raise()),
(2, pytest.does_not_raise()),
(1, pytest.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 this example, the first three test cases should run unexceptionally,
while the fourth should raise ``ZeroDivisionError``.
6 changes: 6 additions & 0 deletions doc/en/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ pytest.raises
.. autofunction:: pytest.raises(expected_exception: Exception, [match], [message])
:with: excinfo

pytest.does_not_raise
~~~~~~~~~~~~~~~~~~~~~

.. autofunction:: pytest.does_not_raise()
:with: excinfo

pytest.deprecated_call
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
43 changes: 43 additions & 0 deletions src/_pytest/python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pprint
import sys
import warnings
from contextlib import contextmanager
from decimal import Decimal
from numbers import Number

Expand Down Expand Up @@ -621,6 +622,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 Expand Up @@ -726,3 +735,37 @@ def __exit__(self, *tp):
if self.match_expr is not None and suppress_exception:
self.excinfo.match(self.match_expr)
return suppress_exception


# builtin pytest.does_not_raise helper


@contextmanager
def does_not_raise():
r'''
This context manager is a complement to ``pytest.raises()`` that does
*not* catch any exceptions raised by the code block.


This is essentially a *no-op* but is useful when
conditionally parametrizing tests that may or may not
raise an error. For example::

@pytest.mark.parametrize('example_input,expectation', [
(3, does_not_raise()),
(2, does_not_raise()),
(1, does_not_raise()),
(0, raises(ZeroDivisionError)),
])
def test_division(example_input, expectation):
"""Test how much I know division."""
with expectation as excinfo:
assert (6 / example_input) is not None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have a warning here for the usage like

with expectation as excinfo: ....
``` - that excinfo will be None

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I updated the sphinx documentation in 77dcac8 regarding execinfo and also added a reference in pytest.raises.


Note that `excinfo` will be *None* when using
``does_not_raise``. In the example above, `execinfo`
will be `None` for the first three runs and
an :class:`ExceptionInfo` instance on last run.
'''

yield
2 changes: 2 additions & 0 deletions src/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from _pytest.python import Module
from _pytest.python import Package
from _pytest.python_api import approx
from _pytest.python_api import does_not_raise
from _pytest.python_api import raises
from _pytest.recwarn import deprecated_call
from _pytest.recwarn import warns
Expand All @@ -50,6 +51,7 @@
"cmdline",
"Collector",
"deprecated_call",
"does_not_raise",
"exit",
"fail",
"File",
Expand Down
38 changes: 38 additions & 0 deletions testing/python/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,44 @@ 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(
"""
import pytest

@pytest.mark.parametrize('example_input,expectation', [
(3, pytest.does_not_raise()),
(2, pytest.does_not_raise()),
(1, pytest.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(
"""
import pytest

@pytest.mark.parametrize('example_input,expectation', [
(0, pytest.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