How to test with parametrize that something is None or not None?

Question:

I have a method that shall return None in some exceptional situations and otherwise something "complicated".
In my dummy MWE below (not my real method!) there are two situations (x==0 and x==10) where None is returned.

    def foo(x: int) -> typing.Optional[float]:
    if x == 0 or x == 10:
        return None
    else:
        return 1/x

I have a unittest to test the "complicated" outcome in detail for "normal" situations. Now, I want to write a test specifically about whether the method returns None for the exceptional situations and something which is not None for the normal situation

@pytest.mark.parametrize("x, target",
                     [(0, None), (10, None), (5, not None)])
def test_foo(x, target):
    assert foo(x) is target

I hoped to have a test case assert foo(5) is not None, but the variable target which was not None is evaluated as True and thus assert foo(x) is True will fail.
So how to test with parametrize that something is None or not None?

Asked By: Qaswed

||

Answers:

Put the desired condition in a function and call that on the result?

@pytest.mark.parametrize("x, validator",[0, lambda x: x is None])
def test_foo(x, validator):
    assert validator(foo(x))

Of course, you can have two real functions is_none and not_none. Signals intent clearly enough.

Alternatively, put some custom logic in your test fn, and use an Enum with two entries NONE and NOT_NONE to signal intent.

Or most simply, just write the test explicitly:

@pytest.mark.parametrize("x, is_none", [0, False])
def test_foo_returns_none(x, is_none):
    res = foo(x)
    if is_none:
        assert res is None
    else:
        assert res is not None

With all that said, you really should look at hypothesis as the answer says. It can be a fiddle to set up for some functions, but it’s a really great tool. It can also be used to find pathological cases, which is otherwise pretty hard.

Answered By: 2e0byo

I think you should really take a look at hypothesis:
(just run pytest as usual to test)

from hypothesis import given 
import hypothesis.strategies as st


def foo(x: int) -> typing.Optional[float]:
    if x == 0 or x == 10:
        return None
    else:
        return 1/x
    
@given(x=st.integers(min_value=14))
def test_foo(x: int) -> None:
    assert foo(x) is not None

Here I added a precondition that if the integer is at least 14 then foo will not return None. Similarly you can add various preconditions easily, and get the benefit of working with symbolic values rather than concrete examples (needed to be written manually)

Answered By: OrenIshShalom