For our API, I started using hypothesis on top pydantic, in order to enhance test test suite with autogenerated data. Our protocol is hierarchical, so comes easy to me to describe it with nested classes in pydantic:
from pydantic import BaseModel, Field, conint
from hypothesis import given, strategies as st
class Inner(BaseModel):
start: conint(ge=0, le=9) = Field(description="starting value")
end: conint(ge=0, le=9) = Field(description="it should be greater than start")
class Outer(BaseModel):
inner: Inner = Field(description="I level class")
@given(st.builds(Outer))
def test_shema(schema):
assert schema.inner.start <= schema.inner.end, "Nope!"
test_shema()
All works as expected, but I'm trying to enrich the semantic with some strategies, for example a rule between inner.start
and inner.end
. In this example one should be greater.
tentative I:
Generate values in separate ranges:
roles = {'inner.start':st.integers(max_value=5), 'inner.end':st.integers(min_value=5)}
@given(st.builds(Outer, **roles))
For this approach I'm not able to specify the field in the inner dataclass, this solution simply doesn't work. Tried nested dicts, dotted notation.
tentative II: Apply a correction to generated test function like this, but only in the test function:
if schema.inner.start >= schema.inner.end:
schema.inner.start ,schema.inner.end = schema.inner.end ,schema.inner.start
This can be a valid approach, but how to describe it to hypothesis?
Starting from official documentation I cannot find how to define strategies for nested classes or put together the composite in order to apply some logic to the generated schema.
What about this?
@given(
st.builds(
Outer,
inner=st.builds(
Inner,
start=st.integers(min_value=0, max_value=5),
end=st.integers(min_value=5, max_value=9),
),
)
)
The documentation shows that you can use a strategy for an arg
or kwarg
of builds()
:
e.g.
builds(target, integers(), flag=booleans())
would draw an integeri
and a booleanb
and calltarget(i, flag=b)
.
So, you could use the builds()
strategy to provide the inner
kwarg
(pydantic expects kwargs
in the __init__()
), and use a similar approach to customize the start
and end
with the integers()
strategy.
It seems that you need to provide both min_value
and max_value
in your scenario because the integers()
strategy can generate integers outside the ge
and le
. I guess it is because integers()
cannot know that its values will be used to feed the start
and end
fields, which seems acceptable to me.