diff --git a/AUTHORS b/AUTHORS index 35e07dcb4..2ea49219c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -27,6 +27,7 @@ Anthony Shaw Anthony Sottile Anton Lodder Antony Lee +Arel Cordero Armin Rigo Aron Coyle Aron Curzon diff --git a/changelog/4324.doc.rst b/changelog/4324.doc.rst new file mode 100644 index 000000000..5e37a91aa --- /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 787bbe10c..b5d4693ad 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -565,3 +565,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 diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 9b31d4e68..1b643d430 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -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:: diff --git a/testing/python/raises.py b/testing/python/raises.py index 4ff0b51bc..f5827e9b0 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -94,6 +94,54 @@ class TestRaises(object): 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)