Skip to content

Introduce pytest.not_raises #2339

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
wants to merge 1 commit into from
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
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ New Features
* ``pytest.raises`` now asserts that the error message matches a text or regex
with the ``match`` keyword argument. Thanks `@Kriechi`_ for the PR.

* ``pytest.not_raises`` helper to use in parametrized tests that expect exceptions or
not depending on other parameters (`#1830`_).
Thanks `@The-Compiler`_ for the idea and `@nicoddemus`_ for the PR.

* ``pytest.param`` can be used to declare test parameter sets with marks and test ids.
Thanks `@RonnyPfannschmidt`_ for the PR.

Expand Down Expand Up @@ -88,6 +92,7 @@ Bug Fixes

.. _#1407: https://github.com/pytest-dev/pytest/issues/1407
.. _#1512: https://github.com/pytest-dev/pytest/issues/1512
.. _#1830: https://github.com/pytest-dev/pytest/issues/1830
.. _#1874: https://github.com/pytest-dev/pytest/pull/1874
.. _#1952: https://github.com/pytest-dev/pytest/pull/1952
.. _#2007: https://github.com/pytest-dev/pytest/issues/2007
Expand Down
24 changes: 24 additions & 0 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def pytest_namespace():
raises.Exception = pytest.fail.Exception
return {
'raises': raises,
'not_raises': NotRaisesContext,
Copy link
Member

Choose a reason for hiding this comment

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

i have a strong opinion on naming it wont_raise in order to give acroos the semantics better

Copy link
Member Author

@nicoddemus nicoddemus Mar 30, 2017

Choose a reason for hiding this comment

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

I like not_raises due to its similarity to raises, but I don't have a strong opinion on it. @The-Compiler do you mind changing to wont_raise?

Copy link
Member

Choose a reason for hiding this comment

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

I think both sound weird, so no strong opinion here 😆

Copy link
Member

Choose a reason for hiding this comment

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

oh, an additonal note, why do we make it a callable?

i believe making it a singleton context-manager named after exactly the context its going to encompass is the best we can do,

else we might see people wanting to creep in more specific behavior causing grief down the line

Copy link
Member Author

Choose a reason for hiding this comment

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

oh, an additonal note, why do we make it a callable?

It felt more natural, because you can use pytest.raises(...). No problem changing it to a singleton if you feel it is more appropriate.

else we might see people wanting to creep in more specific behavior causing grief down the line

What do you mean? People asking to some weird parameter to it to give additional behavior? Can't we just say no?

Copy link
Member Author

Choose a reason for hiding this comment

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

About wont_raise... I'm having some second thoughts that I would like you to consider @RonnyPfannschmidt:

  1. pytest.raises maps well to assert raises, and so does pytest.not_raises that also maps well to assert not raises.
  2. pytest.wont_raises is a contraction of will not, which is not so nice in general;

Hmm on second thought I think we should ask others for their opinion on this. It is important to nail this down right now than regret it later. I'll send an email to the list! 👍

Copy link
Member

Choose a reason for hiding this comment

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

@nicoddemus wrt feature creep, sometimes it just happens, see the major mess non-strict xfail got us into because we did a too cheap job of handling flaky tests better

we should either clearly limit it, or clearly support passing exception types from the get go

Copy link
Member Author

Choose a reason for hiding this comment

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

we should either clearly limit it, or clearly support passing exception types from the get go

OK I don't have a strong objection here.

Also asked in the mailing list if anyone sees a problem in making this a singleton. 👍

'approx': approx,
'collect': {
'Module': Module,
Expand Down Expand Up @@ -956,6 +957,8 @@ def _idval(val, argname, idx, idfn, config=None):
return str(val)
elif isclass(val) and hasattr(val, '__name__'):
return val.__name__
elif isinstance(val, (RaisesContext, NotRaisesContext)):
return str(val)
return str(argname)+str(idx)

def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
Expand Down Expand Up @@ -1266,6 +1269,27 @@ def __exit__(self, *tp):
self.excinfo.match(self.match_expr)
return suppress_exception

def __str__(self):
return 'raises({0})'.format(self.expected_exception.__name__)


# builtin pytest.not_raises helper

class NotRaisesContext(object):
"""
Dummy helper class for use in parametrized tests, for non-raising
cases.
"""

def __enter__(self):
pass

def __exit__(self, exc_type, exc_val, exc_tb):
pass

def __str__(self):
return 'not_raises'


# builtin pytest.approx helper

Expand Down
3 changes: 1 addition & 2 deletions doc/en/assert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ In the context manager form you may use the keyword argument
... pass
... Failed: Expecting ZeroDivisionError

If you want to write test code that works on Python 2.4 as well,
you may also use two other ways to test for an expected exception::
You may also use two other ways to test for an expected exception::

pytest.raises(ExpectedException, func, *args, **kwargs)
pytest.raises(ExpectedException, "func(*args, **kwargs)")
Expand Down
69 changes: 69 additions & 0 deletions doc/en/parametrize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,75 @@ x=1/y=3.
comma-separated-string syntax is now advertised first because
it's easier to write and produces less line noise.


``pytest.mark.parametrize`` and exception expectations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 3.1

Sometimes you might want to make sure a function raises an exception for
a set of inputs but should not raise anything for another set of inputs.

For example, consider this function:

.. code-block:: python

def check_python_identifier(ident):
"""raise ValueError if ``ident`` is not a valid Python identifier."""


This is a natural job for a ``pytest.mark.parametrize``, so you might write
a test like this:

.. code-block:: python

@pytest.mark.parametrize('ident, valid', [
('foobar', True),
('Foobar1', True),
('Foobar_', True),
('Foo_bar_', True),
('Foo bar_', False),
('foo bar_', False),
('1foo bar_', False),
])
def test_check_python_identifier(ident, valid):
if not valid:
with pytest.raises(ValueError):
check_python_identifier(ident)
else:
check_python_identifier(ident) # should not raise


But this makes us repeat ourselves on the test function.

For these situations, where you need to ``parametrize`` a test function
so that sometimes it raises an exception and others do not, you can use
Copy link
Member

Choose a reason for hiding this comment

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

"and others do not" -> "and sometimes doesn't"?

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, that is better! 👍

the convenient ``pytest.not_raises`` helper:

.. code-block:: python

@pytest.mark.parametrize('ident, valid', [
('foobar', pytest.not_raises()),
('Foobar1', pytest.not_raises()),
('Foobar_', pytest.not_raises()),
('Foo_bar_', pytest.not_raises()),
('Foo bar_', pytest.raises(ValueError)),
('foo bar_', pytest.raises(ValueError)),
('1foo bar_', pytest.raises(ValueError)),
])
def test_check_python_identifier(ident, expectation):
with pytest.raises(expectation):
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't either the function signature be test_check_python_identifier(ident, valid): or the first argument to parameterize be ident, expectation?

Also, shouldn't the with statement be:

    with expectation:
      ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks @wcooley but I'm declining this PR for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

@nicoddemus So I noticed after finding & reading over the mailing list threads...

For the sake of posterity for anyone finding this PR and wondering what to do, (I believe but have not actually tried) this can be achieved with a simple no-op context manager:

import contextlib

@contextlib.contextmanager
def noop():
    yield

Then:

@pytest.mark.parametrize('ident, expectation', [
        ('foobar', noop()),
        ('Foobar1', noop()),
        ('uh-oh', pytest.raises(Exception))])
def test_whatever(ident, expectation):
    with expectation:
       if 'oo' not in ident:
         raise Exception('No OO')

check_python_identifier(ident)

How does it work? ``pytest.not_raises`` is a context-manager which **does nothing**. It is merely
a convenience helper to use in conjunction with ``parametrize`` for testing that functions
raise errors sometimes and other times do not.

.. note::
Do not use ``pytest.not_raises()`` in a non-parametrized test just to
check if some code does not raise something; just let the original code propagate
any exception to make the test fail.

.. _`pytest_generate_tests`:

Basic ``pytest_generate_tests`` example
Expand Down
7 changes: 7 additions & 0 deletions testing/python/metafunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ def test_idmaker_enum(self):
result = idmaker(("a", "b"), [pytest.param(e.one, e.two)])
assert result == ["Foo.one-Foo.two"]

def test_idmaker_raises(self):
from _pytest.python import idmaker
result = idmaker(("amount", "exc"), [pytest.param("foo", pytest.raises(TypeError))])
assert result == ["foo-raises(TypeError)"]
result = idmaker(("amount", "exc"), [pytest.param("foo", pytest.not_raises())])
assert result == ["foo-not_raises"]

@pytest.mark.issue351
def test_idmaker_idfn(self):
from _pytest.python import idmaker
Expand Down
11 changes: 10 additions & 1 deletion testing/python/raises.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ def __call__(self):
for o in gc.get_objects():
assert type(o) is not T


def test_raises_match(self):
msg = r"with base \d+"
with pytest.raises(ValueError, match=msg):
Expand All @@ -133,3 +132,13 @@ def test_raises_match(self):
with pytest.raises(AssertionError, match=expr):
with pytest.raises(ValueError, match=msg):
int('asdf', base=10)


def test_not_raises():
"""pytest.not_raises() does not do anything, just ensure it follows the declared API"""
with pytest.not_raises():
pass
with pytest.raises(ValueError):
with pytest.not_raises():
raise ValueError