Reputation: 2651
I have a function like so (it's actually a class, but that's not relevant given Python's duck typing):
def myfunc(a=None, b=None):
<snip>
Now I want to write a Hypothesis test which always supplies a
, but only sometimes b
.
I tried
from hypothesis import given, strategies as strat
@given(a=strat.booleans())
@given(b=strat.integers(min_value=1) | strat.nothing())
def test_model_properties(self, **kwargs):
myval = myfunc(**kwargs)
<snip>
But it seems that when it gets strat.nothing()
just skips that test run (I get hypothesis.errors.FailedHealthCheck: It looks like your strategy is filtering out a lot of data.
when using that as the sole strategy for b
).
How can I only sometimes supply an argument with a Hypothesis test? Do I need to write two tests, one with b
and one without?
Upvotes: 1
Views: 924
Reputation: 3003
It looks like you want none()
instead of nothing()
:
from hypothesis import given, strategies as strat
@given(a=strat.booleans(), b=strat.none() | strat.integers(min_value=1))
def test_model_properties(self, **kwargs):
myval = myfunc(**kwargs)
...
This is simpler than generating dictionaries to use as **kwargs and a little more efficient too. Ordering of the strategies for b
is also important - putting none()
first ensures that the minimal example will be a=False, b=None
instead of a=False, b=1
.
Also note that applying @given
multiple times is very inefficient compared to a single use, and actually deprecated since version 3.34.0.
Upvotes: 1
Reputation: 2651
jacq's answer put me on the right track - the selection of keywords needs to be its own strategy.
With the standard dictionary
std = {'a': strat.booleans()}
and the optional dictionary
opt = {
'b': strat.integers(),
'c': strat.integers(),
}
I can then use chained list comprehension for all the possible "optional argument combinations":
# chain.from_iterable may or may not be faster; it doesn't matter for me.
optional = [combo
for n in range(len(opt.items()))
for combo in itertools.combinations(opt.items(), n+1)]
That generates the key-value tuples for b
, c
, and (b, c)
.
In order to draw a set of values, we need to get one of those options, which can be done with sampled_from(optional)
. With the obtained tuples, we must draw from the strategies within, in addition to those in the std
dictionary.
strat.sampled_from(optional).flatmap(
lambda x: strat.fixed_dictionaries(
{**std, **dict(x)}
)
)
This can all be wrapped in a function, let's call it valid_values()
. You can't use @given(valid_values())
if you specify *args
or **kwargs
in the signature of the wrapped function.
As a result, test_model_properties(self, **kwargs)
becomes test_model_properties(self, kwargs)
(and you can use @given(kwargs=valid_values())
) - by calling the dictionary kwargs
, the rest of the function remains unchanged.
Note: This will not include an empty tuple if you want the possibility of no optional parameters, but that can be appended to the optional
list easily. Alternatively, have range(n+1)
instead of combinations(..., n+1)
, hence including a length of 0.
Upvotes: 2
Reputation: 2120
Your approach is guaranteed to fail, because, as the hypothesis docs imply
hypothesis.strategies.nothing()[source]
This strategy never successfully draws a value and will always reject on an attempt to draw.
your attempt not to provide a value for b
will always fail.
How about this:
from hypothesis.strategies import tuples, integers, booleans, one_of
B = booleans()
I = integers(min_value=0, max_value=10)
one_of(tuples(B), tuples(B, I)).example()
which, over a bunch of trials, gave me outputs such as (True,)
, (False, 9)
, (False, 4)
, (True, 5)
and (False,)
.
You would, of course, use this with *args
rather than **kwargs
.
Upvotes: 2
Reputation: 342
How about:
def myfunc(a=None, b=None):
if b is None:
b=strat.nothing()
# Or whatever you would like b to be when you don't supply an argument
else:
<snip>
So you let b
being the default value (in this case, None
) trigger an 'if' condition inside myfunc()
that sets it to something else.
Upvotes: -1