Houston, TX ShayHill@FoundationSafety
.com
832-287-1336
Test With Random Input in Python
Any time you try to write a function, you got fifty ways you’re gonna mess up. If you think of twenty-five of them, then you’re a genius.

All Software Has Bugs

Random tests are a tool to find bugs you didn’t anticipate. There are many ways to accomplish this, and a few libraries dedicated to it. Here is one simple way that works in Pytest.

The Function We’ll Test

def my_floor(x: float) -> int:
    """Return the floor of x as an integer.

    :param x: The number to floor.
    :return: The largest integer less than or equal to x.
    """
    return int(x)

If you know anything about the floor function, you know exactly when that’s going to fail, but let’s pretend we don’t.

Generate Test Input

We’ll test my_floor by feeding it random numbers and comparing the output to math.floor. To do that, we’ll need random numbers, so write a Python function to generate them.

Note that this is not a pytest fixture.

import random
from collections.abc import Iterator

def iter_random_floats() -> Iterator[float]:
    """Generate random positive and negative numbers."""
    # some floats
    yield from (random.uniform(-1e6, 1e6) for _ in range(20))
    # some integers
    yield from (random.randint(-1000, 1000) for _ in range(20))
    # even where these are included in our random range, likehood of any specific
    # value is very low, throw in some of the usual suspects for breaking things.
    yield 0
    yield math.inf
    yield -math.inf

Feed the Test Function

Now we can write our test function using the pytest.mark.parametrize decorator. This decorator takes some flexible arguments, but here we’ll only need two simple arguments:

  • a parameter name as a string
  • an iterable of values to use for that parameter

Generate the iterable with a call to iter_random_floats. This is a standard function call without any obscure rules. You can pass arguments to your function to constrain your random variables or ask for a specific number of values. You could also use something like (x/50 for x in range(-100, 100)) or random.sample(range(-100, 100), 20) or [0, math.inf, -math.inf]. Any iterable will do.

Remember to include the parameter name in the function signature—like a pytest fixture.

import math
import pytest

@pytest.mark.parametrize("x", iter_random_floats())
def test_v_math(x: float) -> None:
    """For float input, return the same as math.floor."""
    assert my_floor(x) == math.floor(x)

Run the Tests

When we run this with pytest, we get nice output, telling us which inputs succeeded and which failed. We can use this to find bugs and to re-test against values that failed in the past.

tests/test_my_floor.py::test_v_math[-6399.586] FAILED
tests/test_my_floor.py::test_v_math[5941.9685] PASSED
tests/test_my_floor.py::test_v_math[1857.5379] PASSED
tests/test_my_floor.py::test_v_math[-1603.95252] FAILED
tests/test_my_floor.py::test_v_math[5624.3452] PASSED
tests/test_my_floor.py::test_v_math[-6841.8029] FAILED
tests/test_my_floor.py::test_v_math[477] PASSED
tests/test_my_floor.py::test_v_math[-582] PASSED
tests/test_my_floor.py::test_v_math[-530] PASSED
tests/test_my_floor.py::test_v_math[365] PASSED
...
tests/test_my_floor.py::test_v_math[0] PASSED
tests/test_my_floor.py::test_v_math[inf] FAILED
tests/test_my_floor.py::test_v_math[-inf] FAILED

If we scroll down, we get specific information about each failure. In this case, we got assertion errors for negative floats and overflow errors for positive and negative infinity. The input values are shown there too.

________ test_v_math[-inf] _________

x = -inf

    def my_floor(x: float) -> int:
>       return int(x)
E       OverflowError: cannot convert float infinity to integer

tests\test_my_floor.py:12: OverflowError

Use random tests to throw millions of wrenches into the gears. You may find a few surprises before your users do.