pytest: RFC: parametrizing conditional raising with pytest.raises
Currently, parametrizing conditional raising of an exception is rather cumbersome. Consider this:
def foo(inp):
if inp:
raise ValueError("Just a silly example")
When we write a test for it, we need to repeat ourselves:
@pytest.mark.parametrize('inp, raising', [(True, True), (False, False)])
def test_foo(raising):
if raising:
with pytest.raises(ValueError):
foo(inp)
else:
foo(inp)
An easy way to solve this would be to add something like an activated
argument to pytest.raises
, where it simply does nothing:
@pytest.mark.parametrize('inp, raising', [(True, True), (False, False)])
def test_foo(inp, raising):
with pytest.raises(FooError, activated=raising):
foo(inp)
But then sometimes, it’s handy to parametrize the exception too - consider this:
def bar(inp):
if inp == 1:
raise ValueError
elif inp == 2:
raise TypeError
else:
pass
and the test:
@pytest.mark.parametrize('inp, exception', [(1, ValueError), (2, TypeError),
(3, None)])
def test_bar(inp, exception):
if exception is None:
bar(inp)
else:
with pytest.raises(exception):
bar(inp)
So maybe we could just use None
as a special value where pytest.raises
does
nothing?
@pytest.mark.parametrize('inp, exception', [(1, ValueError), (2, TypeError),
(3, None)])
def test_bar(inp, exception):
with pytest.raises(exception):
bar(inp)
Or if we’re worried about None
being accidentally used (though I’m not sure
how?), we could use a special value, potentially pytest.raises.nothing
or so
😆
Opinions?
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 15
- Comments: 71 (58 by maintainers)
Commits related to this issue
- Introduce pytest.not_raises Fix #1830 — committed to nicoddemus/pytest by nicoddemus 7 years ago
- Add test from #1830 — committed to massich/pytest by massich 6 years ago
- Add test from #1830 — committed to massich/pytest by massich 6 years ago
- Adds `does_not_raise` context manager Addressing issues #4324 and #1830 — committed to arel/pytest by arel 5 years ago
- Adds `does_not_raise` context manager Addressing issues #4324 and #1830 — committed to arel/pytest by arel 5 years ago
- Adds `does_not_raise` context manager Addressing issues #4324 and #1830 — committed to arel/pytest by arel 5 years ago
fortunately, if you really want this behaviour you can get it today from the standard library:
nullcontext
ExitStack
contextlib2
For anyone looking for a solution to this now before people agree on naming, you can wrap
pytest.raises
to supportNone
like this:Then you can use it with
parameterize
:I think
None
being considered bypytest.raises
as doing nothing seems sensible.how about something completely different
like a different context manager, so that instead of parameterizing with booleans, we pass different contexts
Just needed this same functionality but surprised to know that, even though almost all alternatives discussed would be good enough for the job (at least for me), none was implemented.
It seems though that Python 3.7 onwards will have a null context manager that could be used in this case too: https://stackoverflow.com/a/45187287
Have found myself wanting this exact functionality a few times now. Seems there was a PR put together a few months back, but it was closed. 😕 Would be really nice to see something coalesce. What seems to be the blocker to implementing this in some form?
Just my 2c, this is going to get abused
The original example in the issue would really be two tests as it is testing two behaviors (one with acceptable inputs and one with unacceptable (exceptional) inputs)
The reason I say this is going to be abused is we had a similar context manager in testify and found that developers (especially beginners) would add it to every test despite it being a noop. Their thought being that it is better to be explicit that it doesn’t raise but the mistake being that an exception already failed the test.
EDIT (for example):
when really they wanted just
I was planning to put some time on it ~this weekend~ and also modify the warnings. last weekend did not happen. Lets see next 😉
@massich
See the mailing list threads: https://mail.python.org/pipermail/pytest-dev/2017-March/thread.html https://mail.python.org/pipermail/pytest-dev/2017-April/thread.html
The main options were:
with pytest.raises(None):
with pytest.raises(pytest.NoException):
or similar (like the above, but it makes it more explicit, and it won’t be done by accident)with pytest.does_not_raise:
or similar (which means you need to parametrize the whole “expectation”)Then it was bikeshedded to death. 😢
(Honestly, I stopped caring. I just started adding stuff to my own testsuite instead of contributing them to pytest. I’d still be happy for this to be added, but if it ends like this, I’m out.)
Oh sorry, I wasn’t very clear then. After seeing @RonnyPfannschmidt’s response, I think the best approach is to just provide a new context manager which just ensures nothing is raised. This way users can use it in the parametrization:
Just to clarify, this does not require changes to
pytest.raises
at all, only creatingdoes_not_raise
which should be very simple.I provided a full recipe here including
contextlib2
if someone wants to copypaste into docs@Sup3rGeo my problem is it encourages two bad patterns:
do_not_raise
when it is unnecessary)is it insufficient to
from contextlib import nullcontext as do_not_raise
?Let’s consider the scenarios of having/not having the noop context manager (considering here
do_not_raise
):Worst thing that can happen if we DO have
do_not_raise
: a1 - Proponents ofpytest.raises(None)
won’t be 100% satisfied. But maybe still 80% satisfied? a2 - Beginners can abuse it. This can happen with a lot of things as well, but just because some users can abuse it, it is not fair to prevent all the other users from having it.Worst thing that can happen if we DON’T have
do_not_raise
b1 - Increasing the LOC and maintenance passive where we require to parametrize on exception raise. b2 - Not consistent withwarns
in terms of noop.Bottom line: Just implement it - its a small and uncertain harm (I would doubt someone will complain) compared to a big and certain benefit (many happy users).
Friendly nudge. Ran into this problem again and figured I’d put it back on people’s radar. 😉
Clearly this is desirable to many. Any ideas how we can unstick this one? 😄
@arel magic numbers are any time when one uses a general value that encodes specific meaning without naming it
so using None to mean “throws no exception” makes it a magical constant
given that it needs a new name to encode new behaviour, its far more simple to just make a new function with a new name and keeping the logic paths more simple
By the way, something which wasn’t mentioned yet: The problem with having a separate context manager like
does_not_raise
is that you have to parametrize the whole expectation like so:However, if you want to use other nice features of
pytest.raises
likematch
(see above), this gets really cumbersome, and your message is “far away” from the rest of your test.So my vote would still be for
with pytest.raises(pytest.NoException):
or the (simpler but less explicit)with pytest.raises(None):
, for very much practical rather than ideological reasons 😉@RonnyPfannschmidt
Yeah I agree, and I think your proposal fits nicely with what @The-Compiler had in mind. @The-Compiler, thoughts?
Here is an example which I think also illustrates @The-Compiler’s use case, taken from pytest-qt:
This could be changed to:
I see your point, but are you arguing for us to drop this topic because your solution is enough? In that case I disagree, it is still worth discussing how change
pytest.raises
(or add a newpytest.no_raise
). If on the other hand you are just pointing out an alternative, that’s perfectly valid and welcome, but doesn’t bar the current discussion.