180

Let's assume we have smth like that :

import py, pytest

ERROR1 = ' --- Error : value < 5! ---'
ERROR2 = ' --- Error : value > 10! ---'

class MyError(Exception):
    def __init__(self, m):
        self.m = m

    def __str__(self):
        return self.m

def foo(i):
    if i < 5:
        raise MyError(ERROR1)
    elif i > 10:
        raise MyError(ERROR2)
    return i


# ---------------------- TESTS -------------------------
def test_foo1():
    with pytest.raises(MyError) as e:
        foo(3)
    assert ERROR1 in str(e)

def test_foo2():
    with pytest.raises(MyError) as e:
        foo(11)
    assert ERROR2 in str(e)

def test_foo3():
        ....
        foo(7)
         ....

Q: How can I make test_foo3() to test, that no MyError is raised? It's obvious, that i could just test :

def test_foo3():
    assert foo(7) == 7

but i want to test that via pytest.raises(). Is is possible someway? For example: in a case, that function "foo" has no return-value at all,

def foo(i):
    if i < 5:
        raise MyError(ERROR1)
    elif i > 10:
        raise MyError(ERROR2)

it could make sense to test this way, imho.

paraklet
  • 1,887
  • 2
  • 13
  • 6
  • 1
    It looks like searching for a problem, the code testing `foo(7)` is fine. You will get the right message and it will be easier to debug with all the pytest output. The suggestion you forced from @Faruk (`'Unexpected error...'`) says nothing about the error and you will et stuck. The only thing you can do to make it better is stating your intention like `test_foo3_works_on_integers_within_range()`. – dhill Dec 11 '13 at 13:53
  • Related: [Python unittest - opposite of assertRaises?](https://stackoverflow.com/q/4319825/95735) – Piotr Dobrogost Feb 22 '21 at 18:43
  • Does this answer your question? [Python unittest - opposite of assertRaises?](https://stackoverflow.com/questions/4319825/python-unittest-opposite-of-assertraises) – Piotr Dobrogost Aug 27 '21 at 08:28

4 Answers4

216

A test will fail if it raises any kind of unexpected Exception. You can just invoke foo(7) and you will have tested that no MyError is raised. So, following will suffice:

def test_foo3():
    foo(7)

If you want to be explicit and write an assert statement for this, you can do:

def test_foo3():
    try:
        foo(7)
    except MyError:
        pytest.fail("Unexpected MyError ..")
Faruk Sahin
  • 8,406
  • 5
  • 28
  • 34
  • 7
    Thanks, it works, but it seems to be more a hack, than a clean solution. For example, test for foo(4) will fail, but not due to assertion error. – paraklet Nov 29 '13 at 11:28
  • 3
    test for foo(4) will fail because it will throw an exception which was not expected. One other way would be to wrap it in a try catch block and fail with a specific message. I'll update my answer. – Faruk Sahin Nov 29 '13 at 13:23
  • 1
    If you have a lot of cases like this it might be useful to write that in a simple function: ``` def not_raises(error_class, func, *args, **kwargs): ... ``` Or you can write a with-like approach like pytest does. If you do I suggest you write a PR with this to benefit all. :) (The repository is in [bitbucket](https://bitbucket.org/hpk42/pytest)). – Bruno Oliveira Dec 01 '13 at 11:43
  • 12
    @paraklet - pytest's main tagline is ["no-boilerplate testing"](http://pytest.readthedocs.org/en/latest/). It's very much in pytest's spirit to enable you to write tests like in Faruk's first example while pytest handles the details for you. To me, the first example is the "clean solution" and second seems unnecessarily verbose. – Nick Chammas Dec 19 '15 at 01:05
  • 7
    I like code to be readable. If I see `pytest.notRaises()` I see clearly that the purpose of the test is checking that an exception is not thrown. If I just execute the code and no assert follows my first thought would be "here something's missing...". Yes, I could write a comment for that, but I prefer code to be self explanatory rather than comments. – xavier Feb 24 '21 at 15:17
  • The problem here is that if you have a few parts of your function that raises MyError, then you have to do more work to know if the error was raised in the expected place. For example def foo(num): if num == 1: raise ValueError("num 1") elif num == 2: raise ValueError("num 2"). Then if you want to know only if it was raised at num2 and you test it with num1, you will have a bad test. – Gonzalo Nov 09 '22 at 17:16
63

Since this question was answered, the pytest docs have updated info on this subject that's worth mentioning here.

https://docs.pytest.org/en/6.2.x/example/parametrize.html#parametrizing-conditional-raising

It's similar to some of the other answers, but using parametrize and a newer builtin nullcontext that makes the solution really clean.

A potential Python3.7+ only example would look like:

from contextlib import nullcontext as does_not_raise
import pytest


@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

Using parametrize this way makes it possible to combine OP's test cases, like:

@pytest.mark.parametrize(
    "example_input,expectation,message",
    [
        (3, pytest.raises(MyError), ERROR1),
        (11, pytest.raises(MyError), ERROR2),
        (7, does_not_raise(), None),
    ],
)
def test_foo(example_input, expectation, message):
    with expectation as e:
        foo(example_input)
    assert message is None or message in str(e)

Doing it this way allows you to test that it did not raise any exception. nullcontext is meant as a stand in for an optional context manager (pytest.raises, in this case). It's not actually doing anything, so if you wanted to test that it did NOT raise a specific exception, you should see one of the other answers.

NikT
  • 1,590
  • 2
  • 16
  • 29
  • This specifically covers how you can swap out `pytest.raises` in `pytest.parametrize`; it isn’t really by itself an answer to the general question. All that `nullcontext` gives you is something that’ll work with the `with` statement but won’t do anything by itself. – Martijn Pieters Jul 03 '21 at 22:52
  • 2
    @MartijnPieters it is implied that you can answer the general question by using it like this: `with does_not_raise(): foo(7)`. In my opinion this prevents the pause one gets when reading an expectation-free test like the bare `foo(7)`. – EliadL Jul 04 '21 at 08:40
  • @MartijnPieters when I saw the original 3 tests in the question, it looked like something that made sense to use `parametrize` for -- which I should have mentioned. I made some edits to relate this back to the question. – NikT Jul 05 '21 at 06:53
43

Building on top of what Oisin mentioned..

You can make a simple not_raises function that acts similar to pytest's raises:

from contextlib import contextmanager

@contextmanager
def not_raises(exception):
  try:
    yield
  except exception:
    raise pytest.fail("DID RAISE {0}".format(exception))

This is fine if you want to stick to having raises having a counterpart and thus your tests being more readable. In essence however you don't really need anything than just running the block of code you want to test on its own line - pytest will fail anyway as soon as that block raises an error.

Pithikos
  • 18,827
  • 15
  • 113
  • 136
  • 6
    I wish this was built into py.test; it would make tests much more readable in some cases. Especially in conjunction with `@pytest.mark.parametrize`. – Arel Aug 22 '17 at 17:58
  • I highly appreciate the sense of code readability in this approach! – Adrian Nov 13 '20 at 06:15
  • This should be the accepted answer. Pity `pytest` does not have something like this already (afaik). – MattSom Aug 03 '21 at 09:21
8

I was curious to see if a not_raises would work. A quick test of this is (test_notraises.py):

from contextlib import contextmanager

@contextmanager
def not_raises(ExpectedException):
    try:
        yield

    except ExpectedException, err:
        raise AssertionError(
            "Did raise exception {0} when it should not!".format(
                repr(ExpectedException)
            )
        )

    except Exception, err:
        raise AssertionError(
            "An unexpected exception {0} raised.".format(repr(err))
        )

def good_func():
    print "hello"


def bad_func():
    raise ValueError("BOOM!")


def ugly_func():
    raise IndexError("UNEXPECTED BOOM!")


def test_ok():
    with not_raises(ValueError):
        good_func()


def test_bad():
    with not_raises(ValueError):
        bad_func()


def test_ugly():
    with not_raises(ValueError):
        ugly_func()

It does seem to work. However I'm not sure if it really reads well in the test.

Oisin
  • 1,483
  • 12
  • 15
  • Updated for python3 https://gist.github.com/oisinmulvihill/45c14271fad7794a4a52516ecb784e69 – Oisin Aug 04 '19 at 13:00
  • 2
    While this adds readable expectation to the test, in my opinion the 'try-except' does not provide added value, since the test will still ERROR rather than FAIL if that exception is raised. Seems better to use `nullcontext` or `pytest.fail` like in the other answers. – EliadL Jul 04 '21 at 08:47