Improve pytest.raises docs (#11578)

This commit is contained in:
Bruno Oliveira 2023-11-04 07:24:57 -03:00 committed by GitHub
parent 13e5ef0102
commit 1e02797d15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 93 additions and 21 deletions

View File

@ -98,6 +98,27 @@ and if you need to have access to the actual exception info you may use:
the actual exception raised. The main attributes of interest are the actual exception raised. The main attributes of interest are
``.type``, ``.value`` and ``.traceback``. ``.type``, ``.value`` and ``.traceback``.
Note that ``pytest.raises`` will match the exception type or any subclasses (like the standard ``except`` statement).
If you want to check if a block of code is raising an exact exception type, you need to check that explicitly:
.. code-block:: python
def test_recursion_depth():
def foo():
raise NotImplementedError
with pytest.raises(RuntimeError) as excinfo:
foo()
assert type(excinfo.value) is RuntimeError
The :func:`pytest.raises` call will succeed, even though the function raises :class:`NotImplementedError`, because
:class:`NotImplementedError` is a subclass of :class:`RuntimeError`; however the following `assert` statement will
catch the problem.
Matching exception messages
~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can pass a ``match`` keyword parameter to the context-manager to test You can pass a ``match`` keyword parameter to the context-manager to test
that a regular expression matches on the string representation of an exception that a regular expression matches on the string representation of an exception
(similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``): (similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``):
@ -115,9 +136,15 @@ that a regular expression matches on the string representation of an exception
with pytest.raises(ValueError, match=r".* 123 .*"): with pytest.raises(ValueError, match=r".* 123 .*"):
myfunc() myfunc()
The regexp parameter of the ``match`` parameter is matched with the ``re.search`` Notes:
function, so in the above example ``match='123'`` would have worked as
well. * The ``match`` parameter is matched with the :func:`re.search`
function, so in the above example ``match='123'`` would have worked as well.
* The ``match`` parameter also matches against `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``.
Matching exception groups
~~~~~~~~~~~~~~~~~~~~~~~~~
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>` You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
method to test for exceptions returned as part of an ``ExceptionGroup``: method to test for exceptions returned as part of an ``ExceptionGroup``:
@ -165,32 +192,55 @@ exception at a specific level; exceptions contained directly in the top
assert not excinfo.group_contains(RuntimeError, depth=2) assert not excinfo.group_contains(RuntimeError, depth=2)
assert not excinfo.group_contains(TypeError, depth=1) assert not excinfo.group_contains(TypeError, depth=1)
There's an alternate form of the :func:`pytest.raises` function where you pass Alternate form (legacy)
a function that will be executed with the given ``*args`` and ``**kwargs`` and ~~~~~~~~~~~~~~~~~~~~~~~
assert that the given exception is raised:
There is an alternate form where you pass
a function that will be executed, along ``*args`` and ``**kwargs``, and :func:`pytest.raises`
will execute the function with the arguments and assert that the given exception is raised:
.. code-block:: python .. code-block:: python
pytest.raises(ExpectedException, func, *args, **kwargs) def func(x):
if x <= 0:
raise ValueError("x needs to be larger than zero")
pytest.raises(ValueError, func, x=-1)
The reporter will provide you with helpful output in case of failures such as *no The reporter will provide you with helpful output in case of failures such as *no
exception* or *wrong exception*. exception* or *wrong exception*.
Note that it is also possible to specify a "raises" argument to This form was the original :func:`pytest.raises` API, developed before the ``with`` statement was
``pytest.mark.xfail``, which checks that the test is failing in a more added to the Python language. Nowadays, this form is rarely used, with the context-manager form (using ``with``)
being considered more readable.
Nonetheless, this form is fully supported and not deprecated in any way.
xfail mark and pytest.raises
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It is also possible to specify a ``raises`` argument to
:ref:`pytest.mark.xfail <pytest.mark.xfail ref>`, which checks that the test is failing in a more
specific way than just having any exception raised: specific way than just having any exception raised:
.. code-block:: python .. code-block:: python
def f():
raise IndexError()
@pytest.mark.xfail(raises=IndexError) @pytest.mark.xfail(raises=IndexError)
def test_f(): def test_f():
f() f()
Using :func:`pytest.raises` is likely to be better for cases where you are
testing exceptions your own code is deliberately raising, whereas using This will only "xfail" if the test fails by raising ``IndexError`` or subclasses.
``@pytest.mark.xfail`` with a check function is probably better for something
like documenting unfixed bugs (where the test describes what "should" happen) * Using :ref:`pytest.mark.xfail <pytest.mark.xfail ref>` with the ``raises`` parameter is probably better for something
or bugs in dependencies. like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies.
* Using :func:`pytest.raises` is likely to be better for cases where you are
testing exceptions your own code is deliberately raising, which is the majority of cases.
.. _`assertwarns`: .. _`assertwarns`:

View File

@ -249,7 +249,9 @@ Marks a test function as *expected to fail*.
:keyword str reason: :keyword str reason:
Reason why the test function is marked as xfail. Reason why the test function is marked as xfail.
:keyword Type[Exception] raises: :keyword Type[Exception] raises:
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test. Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test.
Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works).
:keyword bool run: :keyword bool run:
Whether the test function should actually be executed. If ``False``, the function will always xfail and will Whether the test function should actually be executed. If ``False``, the function will always xfail and will
not be executed (useful if a function is segfaulting). not be executed (useful if a function is segfaulting).

View File

@ -804,11 +804,13 @@ def raises( # noqa: F811
def raises( # noqa: F811 def raises( # noqa: F811
expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]: ) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
r"""Assert that a code block/function call raises an exception. r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception: :param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
The expected exception type, or a tuple if one of multiple possible The expected exception type, or a tuple if one of multiple possible
exception types are expected. exception types are expected. Note that subclasses of the passed exceptions
will also match.
:kwparam str | typing.Pattern[str] | None match: :kwparam str | typing.Pattern[str] | None match:
If specified, a string containing a regular expression, If specified, a string containing a regular expression,
or a regular expression object, that is tested against the string or a regular expression object, that is tested against the string
@ -826,13 +828,13 @@ def raises( # noqa: F811
.. currentmodule:: _pytest._code .. currentmodule:: _pytest._code
Use ``pytest.raises`` as a context manager, which will capture the exception of the given Use ``pytest.raises`` as a context manager, which will capture the exception of the given
type:: type, or any of its subclasses::
>>> import pytest >>> import pytest
>>> with pytest.raises(ZeroDivisionError): >>> with pytest.raises(ZeroDivisionError):
... 1/0 ... 1/0
If the code block does not raise the expected exception (``ZeroDivisionError`` in the example If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example
above), or no exception at all, the check will fail instead. above), or no exception at all, the check will fail instead.
You can also use the keyword argument ``match`` to assert that the You can also use the keyword argument ``match`` to assert that the
@ -845,7 +847,7 @@ def raises( # noqa: F811
... raise ValueError("value must be 42") ... raise ValueError("value must be 42")
The ``match`` argument searches the formatted exception string, which includes any The ``match`` argument searches the formatted exception string, which includes any
`PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``: `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``:
>>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP >>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP
... e = ValueError("value must be 42") ... e = ValueError("value must be 42")
@ -860,6 +862,20 @@ def raises( # noqa: F811
>>> assert exc_info.type is ValueError >>> assert exc_info.type is ValueError
>>> assert exc_info.value.args[0] == "value must be 42" >>> assert exc_info.value.args[0] == "value must be 42"
.. warning::
Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this::
with pytest.raises(Exception): # Careful, this will catch ANY exception raised.
some_function()
Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide
real bugs, where the user wrote this expecting a specific exception, but some other exception is being
raised due to a bug introduced during a refactoring.
Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch
**any** exception raised.
.. note:: .. note::
When using ``pytest.raises`` as a context manager, it's worthwhile to When using ``pytest.raises`` as a context manager, it's worthwhile to
@ -872,7 +888,7 @@ def raises( # noqa: F811
>>> with pytest.raises(ValueError) as exc_info: >>> with pytest.raises(ValueError) as exc_info:
... if value > 10: ... if value > 10:
... raise ValueError("value must be <= 10") ... raise ValueError("value must be <= 10")
... assert exc_info.type is ValueError # this will not execute ... assert exc_info.type is ValueError # This will not execute.
Instead, the following approach must be taken (note the difference in Instead, the following approach must be taken (note the difference in
scope):: scope)::
@ -891,6 +907,10 @@ def raises( # noqa: F811
See :ref:`parametrizing_conditional_raising` for an example. See :ref:`parametrizing_conditional_raising` for an example.
.. seealso::
:ref:`assertraises` for more examples and detailed discussion.
**Legacy form** **Legacy form**
It is possible to specify a callable by passing a to-be-called lambda:: It is possible to specify a callable by passing a to-be-called lambda::