diff --git a/AUTHORS b/AUTHORS index 957a8697311..aa3428a0605 100644 --- a/AUTHORS +++ b/AUTHORS @@ -26,6 +26,7 @@ Anthony Shaw Anthony Sottile Anton Lodder Antony Lee +Arel Cordero Armin Rigo Aron Coyle Aron Curzon diff --git a/changelog/1830.feature.rst b/changelog/1830.feature.rst new file mode 100644 index 00000000000..7a157abc3da --- /dev/null +++ b/changelog/1830.feature.rst @@ -0,0 +1 @@ +A context manager ``does_not_raise`` is added to complement ``raises`` in parametrized tests with conditional raises. diff --git a/changelog/4324.doc.rst b/changelog/4324.doc.rst new file mode 100644 index 00000000000..5e37a91aa38 --- /dev/null +++ b/changelog/4324.doc.rst @@ -0,0 +1 @@ +Document how to use ``raises`` and ``does_not_raise`` to write parametrized tests with conditional raises. diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 98aaeae3b4e..50d615891f7 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -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``. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 92e298a88e8..f2dedbd9745 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -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 ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 9b31d4e688e..a3532a54107 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -4,6 +4,7 @@ import pprint import sys import warnings +from contextlib import contextmanager from decimal import Decimal from numbers import Number @@ -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:: @@ -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 + + 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 diff --git a/src/pytest.py b/src/pytest.py index c0010f166df..16ce2ad706c 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -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 @@ -50,6 +51,7 @@ "cmdline", "Collector", "deprecated_call", + "does_not_raise", "exit", "fail", "File", diff --git a/testing/python/raises.py b/testing/python/raises.py index 4ff0b51bc23..8135c2c3440 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -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)