How to make custom Hypothesis strategy to supply custom objects?

Question:

Suppose I have a class Thing

class Thing:

    def __init__(self, x, y):
        ...

And suppose I have a function which acts on a list of things.

def do_stuff(list_of_things):
    ...

I would like to write unit tests for do_stuff involving different instances of lists of Thing.

Is there a way to define a custom Hypothesis strategy which provides examples of list_of_things for my unit tests on do_stuff?

Asked By: Galen

||

Answers:

You can make use of the hypothesis.strategies.composite object, which you can use as a decorator on a function which returns instances of the class you want to generate test cases on.

Here is a short example where we suppose some class Person which has the properties name and age. The person_objects function uses strategies for each of these attributes using behaviour that is built-in to Hypothesis. Note the @composite above the function header, and that person_objects has a parameter called draw that is a function called on some existing strategies.

from hypothesis import strategies as st, composite
from my_module import Person

# Define a strategy for generating random names
names = st.text(min_size=1, max_size=50)

# Define a strategy for generating random ages
ages = st.integers(min_value=0, max_value=150)

# Define a custom strategy for generating random Person objects
@composite
def person_objects(draw):
    name = draw(names)
    age = draw(ages)
    return Person(name, age)

# Use the generated Person objects in the decorated function
@given(person_objects())
def test_person_objects(person):
    assert isinstance(person, Person)
    assert isinstance(person.name, str)
    assert isinstance(person.age, int)
    assert 0 <= person.age <= 150
Answered By: Galen

No need for a complicated composite strategy; this is a perfect case for the st.builds() strategy:

from hypothesis import given, strategies as st

@given(
    st.lists(
        st.builds(Thing, x=st.integers(), y=st.integers())
    )
)
def test_do_stuff(ls):
    do_stuff(ls)

If Thing.__init__ has type annotations, you can omit strategies for the required arguments and st.builds() will fill them in for you (recursively, if it takes an instance of Foo!). You can also use type annotations in your test, and have Hypothesis infer the strategies entirely:

@given(ls=...)  # literal "...", Python's ellipsis object
def test_do_stuff(ls: list[Thing], tmpdir):  # and tmpdir fixture
    do_stuff(ls)

@given(...)  # this will provide _all_ arguments, so no fixtures
def test_do_stuff(ls: list[Thing]):
    do_stuff(ls)
Answered By: Zac Hatfield-Dodds