0

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?

Galen
  • 1,128
  • 1
  • 14
  • 31
  • 1
    This boils down to "How to make a custom Hypothesis strategy for `Thing`?", since you'll supply that to `hypothesis.strategies.lists` to make a `list[Thing]` value. – chepner Mar 14 '23 at 17:36
  • @chepner Yes, your description of the problem is excellent. – Galen Mar 14 '23 at 17:38
  • 1
    We've reached the end of my practical knowledge of Hypothesis :) But see https://hypothesis.readthedocs.io/en/latest/data.html#composite-strategies for how you can define a strategy that can use, for example, other strategies to create `x` and `y` values so you can return `Thing(x, y)`. – chepner Mar 14 '23 at 17:47
  • I think I figured it out. I will post an answer later. – Galen Mar 14 '23 at 18:30

2 Answers2

1

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
Galen
  • 1,128
  • 1
  • 14
  • 31
1

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)
Zac Hatfield-Dodds
  • 2,455
  • 6
  • 19