Search code examples
pythonpython-3.xtestingautomated-testspython-hypothesis

Getting 'LazyStrategy' object instead of integer in hypothesis when using flatmap


Working with the python testing framework hypothesis, I would like to achieve a rather complex composition of testing strategies: 1. I would like to test against create strings s which consist of a unique character set. 2. Each of these examples I want to run through a function func(s: str, n: int) -> Tuple[str, int] which takes a string s and an integer n as a parameters. Here, I want to make the integer be populated by hypothesis as well, but the maximum value of n should be len(s), the length of s. I've tried to use flatmap, but do not understand it sufficiently yet to get it to work. Here is the minimal example of what I've tried:

from typing import Tuple

from hypothesis import given
from hypothesis.strategies import text, integers


def func(s: str, n: int) -> Tuple[str, int]:
    for i in range(n):
        pass  # I do something here on s, which is not relevant to the question

    return (s, n)

# 1. Create the strategy for unique character strings:
unique_char_str = text().map(lambda s: "".join(set(s)))

#2. Create the complex strategy with flatmap:
confined_tuple_str_int = unique_char_str.flatmap(
    lambda s: func(s, integers(min_value=0, max_value=len(s)))
)

When I try to use this new strategy,


@given(tup=confined_tuple_str_int)
def test_foo(tup):
    pass

I get a FAILED test, claiming

test_foo - TypeError: 'LazyStrategy' object cannot be interpreted as an integer

In the line for i in range(n):, in func, n is not an integer, but a LazyStrategy object.

That tells me, I have some misconception of how flatmap works, but I cannot figure it out on my own.

What do I need to do, to define my testing strategy properly?


Solution

  • With flatmap, you cannot combine two dependent strategies - this can be done using the composite decorator instead:

    @composite
    def confined_tuple_str_int(draw, elements=text()):
        s = draw(lists(characters(), unique=True).map("".join))
        i = draw(integers(min_value=0, max_value=len(s)))
        return func(s, i)
    
    
    @given(confined_tuple_str_int())
    def test_foo(tup):
        print(tup)
    

    The draw argument allows you to get the drawn value of a strategy (e.g. an example) - in this case a unique string - and use it to create a strategy dependent on that value - in this case an integer strategy dependent on the string length. After also drawing an example from that strategy, the composite decorator creates a new strategy from these example values and returns it.

    Disclaimer: I'm new to hypothesis, so my terminology may be a bit off.

    Edit: Updated to make sure that the order of examples is preserved, as suggested by Zac Hatfield-Dodds in the comments.