Add a Hypothesis plugin (#2097)

* Configure Hypothesis

* Hypothesis plugin docs

* Add Hypothesis plugin

Co-authored-by: Samuel Colvin <s@muelcolvin.com>
This commit is contained in:
Zac Hatfield-Dodds
2021-02-11 23:33:11 +11:00
committed by GitHub
parent d0baf0f156
commit 771b0d3d92
14 changed files with 553 additions and 7 deletions
+1
View File
@@ -16,6 +16,7 @@ dist/
.mypy_cache/
test.py
.coverage
.hypothesis
/htmlcov/
/benchmarks/*.json
/docs/.changelog.md
+1
View File
@@ -0,0 +1 @@
Add a [Hypothesis](https://hypothesis.readthedocs.io/) plugin for easier [property-based testing](https://increment.com/testing/in-praise-of-property-based-testing/) with Pydantic's custom types - [usage details here](https://pydantic-docs.helpmanual.io/hypothesis_plugin/)
@@ -0,0 +1,24 @@
import typing
from hypothesis import given, strategies as st
from pydantic import BaseModel, EmailStr, PaymentCardNumber, PositiveFloat
class Model(BaseModel):
card: PaymentCardNumber
price: PositiveFloat
users: typing.List[EmailStr]
@given(st.builds(Model))
def test_property(instance):
# Hypothesis calls this test function many times with varied Models,
# so you can write a test that should pass given *any* instance.
assert 0 < instance.price
assert all('@' in email for email in instance.users)
@given(st.builds(Model, price=st.floats(100, 200)))
def test_with_discount(instance):
# This test shows how you can override specific fields,
# and let Hypothesis fill in any you don't care about.
assert 100 <= instance.price <= 200
+28
View File
@@ -0,0 +1,28 @@
[Hypothesis](https://hypothesis.readthedocs.io/) is the Python library for
[property-based testing](https://increment.com/testing/in-praise-of-property-based-testing/).
Hypothesis can infer how to construct type-annotated classes, and supports builtin types,
many standard library types, and generic types from the
[`typing`](https://docs.python.org/3/library/typing.html) and
[`typing_extensions`](https://pypi.org/project/typing-extensions/) modules by default.
From Pydantic v1.8 and [Hypothesis v5.29.0](https://hypothesis.readthedocs.io/en/latest/changes.html#v5-29-0),
Hypothesis will automatically load support for [custom types](usage/types.md) like
`PaymentCardNumber` and `PositiveFloat`, so that the
[`st.builds()`](https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.builds)
and [`st.from_type()`](https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.from_type)
strategies support them without any user configuration.
### Example tests
```py
{!.tmp_examples/hypothesis_property_based_test.py!}
```
_(This script is complete, it should run "as is")_
### Use with JSON Schemas
To test client-side code, you can use [`Model.schema()`](usage/models.md) with the
[`hypothesis-jsonschema` package](https://pypi.org/project/hypothesis-jsonschema/)
to generate arbitrary JSON instances matching the schema.
For web API testing, [Schemathesis](https://schemathesis.readthedocs.io) provides
a higher-level wrapper and can detect both errors and security vulnerabilities.
+1
View File
@@ -1,6 +1,7 @@
ansi2html==1.6.0
flake8==3.8.4
flake8-quotes==3.2.0
hypothesis==5.44.0
mkdocs==1.1.2
mkdocs-exclude==1.0.2
mkdocs-material==6.2.8
+1
View File
@@ -46,6 +46,7 @@ nav:
- benchmarks.md
- 'Mypy plugin': mypy_plugin.md
- 'PyCharm plugin': pycharm_plugin.md
- 'Hypothesis plugin': hypothesis_plugin.md
- 'Code Generation': datamodel_code_generator.md
- changelog.md
+349
View File
@@ -0,0 +1,349 @@
"""
Register Hypothesis strategies for Pydantic custom types.
This enables fully-automatic generation of test data for most Pydantic classes.
Note that this module has *no* runtime impact on Pydantic itself; instead it
is registered as a setuptools entry point and Hypothesis will import it if
Pydantic is installed. See also:
https://hypothesis.readthedocs.io/en/latest/strategies.html#registering-strategies-via-setuptools-entry-points
https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.register_type_strategy
https://hypothesis.readthedocs.io/en/latest/strategies.html#interaction-with-pytest-cov
https://pydantic-docs.helpmanual.io/usage/types/#pydantic-types
Note that because our motivation is to *improve user experience*, the strategies
are always sound (never generate invalid data) but sacrifice completeness for
maintainability (ie may be unable to generate some tricky but valid data).
Finally, this module makes liberal use of `# type: ignore[<code>]` pragmas.
This is because Hypothesis annotates `register_type_strategy()` with
`(T, SearchStrategy[T])`, but in most cases we register e.g. `ConstrainedInt`
to generate instances of the builtin `int` type which match the constraints.
"""
import contextlib
import ipaddress
import json
import math
from fractions import Fraction
from typing import Callable, Dict, Type, Union, cast, overload
import hypothesis.strategies as st
import pydantic
import pydantic.color
import pydantic.types
# FilePath and DirectoryPath are explicitly unsupported, as we'd have to create
# them on-disk, and that's unsafe in general without being told *where* to do so.
#
# URLs are unsupported because it's easy for users to define their own strategy for
# "normal" URLs, and hard for us to define a general strategy which includes "weird"
# URLs but doesn't also have unpredictable performance problems.
#
# conlist() and conset() are unsupported for now, because the workarounds for
# Cython and Hypothesis to handle parametrized generic types are incompatible.
# Once Cython can support 'normal' generics we'll revisit this.
# Emails
try:
import email_validator
except ImportError: # pragma: no cover
pass
else:
def is_valid_email(s: str) -> bool:
# Hypothesis' st.emails() occasionally generates emails like 0@A0--0.ac
# that are invalid according to email-validator, so we filter those out.
try:
email_validator.validate_email(s, check_deliverability=False)
return True
except email_validator.EmailNotValidError:
return False
# Note that these strategies deliberately stay away from any tricky Unicode
# or other encoding issues; we're just trying to generate *something* valid.
st.register_type_strategy(pydantic.EmailStr, st.emails().filter(is_valid_email)) # type: ignore[arg-type]
st.register_type_strategy(
pydantic.NameEmail,
st.builds(
'{} <{}>'.format, # type: ignore[arg-type]
st.from_regex('[A-Za-z0-9_]+( [A-Za-z0-9_]+){0,5}', fullmatch=True),
st.emails().filter(is_valid_email),
),
)
# PyObject - dotted names, in this case taken from the math module.
st.register_type_strategy(
pydantic.PyObject,
st.sampled_from(
[cast(pydantic.PyObject, f'math.{name}') for name in sorted(vars(math)) if not name.startswith('_')]
),
)
# CSS3 Colors; as name, hex, rgb(a) tuples or strings, or hsl strings
_color_regexes = (
'|'.join(
(
pydantic.color.r_hex_short,
pydantic.color.r_hex_long,
pydantic.color.r_rgb,
pydantic.color.r_rgba,
pydantic.color.r_hsl,
pydantic.color.r_hsla,
)
)
# Use more precise regex patterns to avoid value-out-of-range errors
.replace(pydantic.color._r_sl, r'(?:(\d\d?(?:\.\d+)?|100(?:\.0+)?)%)')
.replace(pydantic.color._r_alpha, r'(?:(0(?:\.\d+)?|1(?:\.0+)?|\.\d+|\d{1,2}%))')
.replace(pydantic.color._r_255, r'(?:((?:\d|\d\d|[01]\d\d|2[0-4]\d|25[0-4])(?:\.\d+)?|255(?:\.0+)?))')
)
st.register_type_strategy(
pydantic.color.Color,
st.one_of(
st.sampled_from(sorted(pydantic.color.COLORS_BY_NAME)),
st.tuples(
st.integers(0, 255),
st.integers(0, 255),
st.integers(0, 255),
st.none() | st.floats(0, 1) | st.floats(0, 100).map('{}%'.format),
),
st.from_regex(_color_regexes, fullmatch=True),
),
)
# Card numbers, valid according to the Luhn algorithm
def add_luhn_digit(card_number: str) -> str:
# See https://en.wikipedia.org/wiki/Luhn_algorithm
for digit in '0123456789':
with contextlib.suppress(Exception):
pydantic.PaymentCardNumber.validate_luhn_check_digit(card_number + digit)
return card_number + digit
raise AssertionError('Unreachable') # pragma: no cover
card_patterns = (
# Note that these patterns omit the Luhn check digit; that's added by the function above
'4[0-9]{14}', # Visa
'5[12345][0-9]{13}', # Mastercard
'3[47][0-9]{12}', # American Express
'[0-26-9][0-9]{10,17}', # other (incomplete to avoid overlap)
)
st.register_type_strategy(
pydantic.PaymentCardNumber,
st.from_regex('|'.join(card_patterns), fullmatch=True).map(add_luhn_digit), # type: ignore[arg-type]
)
# UUIDs
st.register_type_strategy(pydantic.UUID1, st.uuids(version=1)) # type: ignore[arg-type]
st.register_type_strategy(pydantic.UUID3, st.uuids(version=3)) # type: ignore[arg-type]
st.register_type_strategy(pydantic.UUID4, st.uuids(version=4)) # type: ignore[arg-type]
st.register_type_strategy(pydantic.UUID5, st.uuids(version=5)) # type: ignore[arg-type]
# Secrets
st.register_type_strategy(pydantic.SecretBytes, st.binary().map(pydantic.SecretBytes))
st.register_type_strategy(pydantic.SecretStr, st.text().map(pydantic.SecretStr))
# IP addresses, networks, and interfaces
st.register_type_strategy(pydantic.IPvAnyAddress, st.ip_addresses())
st.register_type_strategy(
pydantic.IPvAnyInterface,
st.from_type(ipaddress.IPv4Interface) | st.from_type(ipaddress.IPv6Interface),
)
st.register_type_strategy(
pydantic.IPvAnyNetwork,
st.from_type(ipaddress.IPv4Network) | st.from_type(ipaddress.IPv6Network),
)
# We hook into the con***() functions and the ConstrainedNumberMeta metaclass,
# so here we only have to register subclasses for other constrained types which
# don't go via those mechanisms. Then there are the registration hooks below.
st.register_type_strategy(pydantic.StrictBool, st.booleans())
st.register_type_strategy(pydantic.StrictStr, st.text()) # type: ignore[arg-type]
# Constrained-type resolver functions
#
# For these ones, we actually want to inspect the type in order to work out a
# satisfying strategy. First up, the machinery for tracking resolver functions:
RESOLVERS: Dict[type, Callable[[type], st.SearchStrategy]] = {} # type: ignore[type-arg]
@overload
def _registered(typ: Type[pydantic.types.T]) -> Type[pydantic.types.T]:
pass
@overload
def _registered(typ: pydantic.types.ConstrainedNumberMeta) -> pydantic.types.ConstrainedNumberMeta:
pass
def _registered(
typ: Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta]
) -> Union[Type[pydantic.types.T], pydantic.types.ConstrainedNumberMeta]:
# This function replaces the version in `pydantic.types`, in order to
# effect the registration of new constrained types so that Hypothesis
# can generate valid examples.
pydantic.types._DEFINED_TYPES.add(typ)
for supertype, resolver in RESOLVERS.items():
if issubclass(typ, supertype):
st.register_type_strategy(typ, resolver(typ)) # type: ignore
return typ
raise NotImplementedError(f'Unknown type {typ!r} has no resolver to register') # pragma: no cover
def resolves(
typ: Union[type, pydantic.types.ConstrainedNumberMeta]
) -> Callable[[Callable[..., st.SearchStrategy]], Callable[..., st.SearchStrategy]]: # type: ignore[type-arg]
def inner(f): # type: ignore
assert f not in RESOLVERS
RESOLVERS[typ] = f
return f
return inner
# Type-to-strategy resolver functions
@resolves(pydantic.Json)
@resolves(pydantic.JsonWrapper)
def resolve_json(cls): # type: ignore[no-untyped-def]
try:
inner = st.none() if cls.inner_type is None else st.from_type(cls.inner_type)
except Exception:
finite = st.floats(allow_infinity=False, allow_nan=False)
inner = st.recursive(
base=st.one_of(st.none(), st.booleans(), st.integers(), finite, st.text()),
extend=lambda x: st.lists(x) | st.dictionaries(st.text(), x),
)
return st.builds(
json.dumps,
inner,
ensure_ascii=st.booleans(),
indent=st.none() | st.integers(0, 16),
sort_keys=st.booleans(),
)
@resolves(pydantic.ConstrainedBytes)
def resolve_conbytes(cls): # type: ignore[no-untyped-def] # pragma: no cover
min_size = cls.min_length or 0
max_size = cls.max_length
if not cls.strip_whitespace:
return st.binary(min_size=min_size, max_size=max_size)
# Fun with regex to ensure we neither start nor end with whitespace
repeats = '{{{},{}}}'.format(
min_size - 2 if min_size > 2 else 0,
max_size - 2 if (max_size or 0) > 2 else '',
)
if min_size >= 2:
pattern = rf'\W.{repeats}\W'
elif min_size == 1:
pattern = rf'\W(.{repeats}\W)?'
else:
assert min_size == 0
pattern = rf'(\W(.{repeats}\W)?)?'
return st.from_regex(pattern.encode(), fullmatch=True)
@resolves(pydantic.ConstrainedDecimal)
def resolve_condecimal(cls): # type: ignore[no-untyped-def]
min_value = cls.ge
max_value = cls.le
if cls.gt is not None:
assert min_value is None, 'Set `gt` or `ge`, but not both'
min_value = cls.gt
if cls.lt is not None:
assert max_value is None, 'Set `lt` or `le`, but not both'
max_value = cls.lt
s = st.decimals(min_value, max_value, allow_nan=False)
if cls.lt is not None:
s = s.filter(lambda d: d < cls.lt)
if cls.gt is not None:
s = s.filter(lambda d: cls.gt < d)
return s
@resolves(pydantic.ConstrainedFloat)
def resolve_confloat(cls): # type: ignore[no-untyped-def]
min_value = cls.ge
max_value = cls.le
exclude_min = False
exclude_max = False
if cls.gt is not None:
assert min_value is None, 'Set `gt` or `ge`, but not both'
min_value = cls.gt
exclude_min = True
if cls.lt is not None:
assert max_value is None, 'Set `lt` or `le`, but not both'
max_value = cls.lt
exclude_max = True
return st.floats(min_value, max_value, exclude_min=exclude_min, exclude_max=exclude_max, allow_nan=False)
@resolves(pydantic.ConstrainedInt)
def resolve_conint(cls): # type: ignore[no-untyped-def]
min_value = cls.ge
max_value = cls.le
if cls.gt is not None:
assert min_value is None, 'Set `gt` or `ge`, but not both'
min_value = cls.gt + 1
if cls.lt is not None:
assert max_value is None, 'Set `lt` or `le`, but not both'
max_value = cls.lt - 1
if cls.multiple_of is None or cls.multiple_of == 1:
return st.integers(min_value, max_value)
# These adjustments and the .map handle integer-valued multiples, while the
# .filter handles trickier cases as for confloat.
if min_value is not None:
min_value = math.ceil(Fraction(min_value) / Fraction(cls.multiple_of))
if max_value is not None:
max_value = math.floor(Fraction(max_value) / Fraction(cls.multiple_of))
return st.integers(min_value, max_value).map(lambda x: x * cls.multiple_of)
@resolves(pydantic.ConstrainedStr)
def resolve_constr(cls): # type: ignore[no-untyped-def] # pragma: no cover
min_size = cls.min_length or 0
max_size = cls.max_length
if cls.regex is None and not cls.strip_whitespace:
return st.text(min_size=min_size, max_size=max_size)
if cls.regex is not None:
strategy = st.from_regex(cls.regex)
if cls.strip_whitespace:
strategy = strategy.filter(lambda s: s == s.strip())
elif cls.strip_whitespace:
repeats = '{{{},{}}}'.format(
min_size - 2 if min_size > 2 else 0,
max_size - 2 if (max_size or 0) > 2 else '',
)
if min_size >= 2:
strategy = st.from_regex(rf'\W.{repeats}\W')
elif min_size == 1:
strategy = st.from_regex(rf'\W(.{repeats}\W)?')
else:
assert min_size == 0
strategy = st.from_regex(rf'(\W(.{repeats}\W)?)?')
if min_size == 0 and max_size is None:
return strategy
elif max_size is None:
return strategy.filter(lambda s: min_size <= len(s))
return strategy.filter(lambda s: min_size <= len(s) <= max_size)
# Finally, register all previously-defined types, and patch in our new function
for typ in pydantic.types._DEFINED_TYPES:
_registered(typ)
pydantic.types._registered = _registered
+32 -7
View File
@@ -20,8 +20,10 @@ from typing import (
TypeVar,
Union,
cast,
overload,
)
from uuid import UUID
from weakref import WeakSet
from . import errors
from .utils import import_string, update_not_none
@@ -109,6 +111,30 @@ if TYPE_CHECKING:
ModelOrDc = Type[Union['BaseModel', 'Dataclass']]
T = TypeVar('T')
_DEFINED_TYPES: 'WeakSet[type]' = WeakSet()
@overload
def _registered(typ: Type[T]) -> Type[T]:
pass
@overload
def _registered(typ: 'ConstrainedNumberMeta') -> 'ConstrainedNumberMeta':
pass
def _registered(typ: Union[Type[T], 'ConstrainedNumberMeta']) -> Union[Type[T], 'ConstrainedNumberMeta']:
# In order to generate valid examples of constrained types, Hypothesis needs
# to inspect the type object - so we keep a weakref to each contype object
# until it can be registered. When (or if) our Hypothesis plugin is loaded,
# it monkeypatches this function.
# If Hypothesis is never used, the total effect is to keep a weak reference
# which has minimal memory usage and doesn't even affect garbage collection.
_DEFINED_TYPES.add(typ)
return typ
class ConstrainedBytes(bytes):
strip_whitespace = False
@@ -134,10 +160,7 @@ class StrictBytes(ConstrainedBytes):
def conbytes(*, strip_whitespace: bool = False, min_length: int = None, max_length: int = None) -> Type[bytes]:
# use kwargs then define conf in a dict to aid with IDE type hinting
namespace = dict(strip_whitespace=strip_whitespace, min_length=min_length, max_length=max_length)
return type('ConstrainedBytesValue', (ConstrainedBytes,), namespace)
T = TypeVar('T')
return _registered(type('ConstrainedBytesValue', (ConstrainedBytes,), namespace))
# This types superclass should be List[T], but cython chokes on that...
@@ -272,7 +295,7 @@ def constr(
curtail_length=curtail_length,
regex=regex and re.compile(regex),
)
return type('ConstrainedStrValue', (ConstrainedStr,), namespace)
return _registered(type('ConstrainedStrValue', (ConstrainedStr,), namespace))
class StrictStr(ConstrainedStr):
@@ -344,7 +367,7 @@ class ConstrainedNumberMeta(type):
if new_cls.lt is not None and new_cls.le is not None:
raise errors.ConfigError('bounds lt and le cannot be specified at the same time')
return new_cls
return _registered(new_cls) # type: ignore
class ConstrainedInt(int, metaclass=ConstrainedNumberMeta):
@@ -616,7 +639,7 @@ class JsonWrapper:
class JsonMeta(type):
def __getitem__(self, t: Type[Any]) -> Type[JsonWrapper]:
return type('JsonWrapperValue', (JsonWrapper,), {'inner_type': t})
return _registered(type('JsonWrapperValue', (JsonWrapper,), {'inner_type': t}))
class Json(metaclass=JsonMeta):
@@ -726,6 +749,8 @@ class SecretBytes:
class PaymentCardBrand(str, Enum):
# If you add another card type, please also add it to the
# Hypothesis strategy in `pydantic._hypothesis_plugin`.
amex = 'American Express'
mastercard = 'Mastercard'
visa = 'Visa'
+1
View File
@@ -1,5 +1,6 @@
[tool:pytest]
testpaths = tests
addopts = -p no:hypothesispytest
filterwarnings =
error
ignore::DeprecationWarning:distutils
+2
View File
@@ -114,6 +114,7 @@ setup(
'Operating System :: POSIX :: Linux',
'Environment :: Console',
'Environment :: MacOS X',
'Framework :: Hypothesis',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Internet',
],
@@ -134,4 +135,5 @@ setup(
'dotenv': ['python-dotenv>=0.10.4'],
},
ext_modules=ext_modules,
entry_points={'hypothesis': ['_ = pydantic._hypothesis_plugin']},
)
+3
View File
@@ -9,6 +9,9 @@ from types import FunctionType
import pytest
from _pytest.assertion.rewrite import AssertionRewritingHook
# See https://hypothesis.readthedocs.io/en/latest/strategies.html#interaction-with-pytest-cov
pytest_plugins = ['hypothesis.extra.pytestplugin']
def _extract_source_code_from_function(function):
if function.__code__.co_argcount:
+1
View File
@@ -1,6 +1,7 @@
black==20.8b1
flake8==3.8.4
flake8-quotes==3.2.0
hypothesis==5.44.0
isort==5.7.0
mypy==0.800
pycodestyle==2.6.0
+1
View File
@@ -1,4 +1,5 @@
coverage==5.4
hypothesis==5.44.0
# pin importlib-metadata as upper versions need typing-extensions to work if on python < 3.8
importlib-metadata==3.1.0;python_version<"3.8"
mypy==0.800
+108
View File
@@ -0,0 +1,108 @@
import typing
import pytest
from hypothesis import given, strategies as st
import pydantic
from pydantic.networks import import_email_validator
def gen_models():
class MiscModel(pydantic.BaseModel):
# Each of these models contains a few related fields; the idea is that
# if there's a bug we have neither too many fields to dig through nor
# too many models to read.
obj: pydantic.PyObject
color: pydantic.color.Color
json_any: pydantic.Json
class StringsModel(pydantic.BaseModel):
card: pydantic.PaymentCardNumber
secbytes: pydantic.SecretBytes
secstr: pydantic.SecretStr
class UUIDsModel(pydantic.BaseModel):
uuid1: pydantic.UUID1
uuid3: pydantic.UUID3
uuid4: pydantic.UUID4
uuid5: pydantic.UUID5
class IPvAnyAddress(pydantic.BaseModel):
address: pydantic.IPvAnyAddress
class IPvAnyInterface(pydantic.BaseModel):
interface: pydantic.IPvAnyInterface
class IPvAnyNetwork(pydantic.BaseModel):
network: pydantic.IPvAnyNetwork
class StrictNumbersModel(pydantic.BaseModel):
strictbool: pydantic.StrictBool
strictint: pydantic.StrictInt
strictfloat: pydantic.StrictFloat
strictstr: pydantic.StrictStr
class NumbersModel(pydantic.BaseModel):
posint: pydantic.PositiveInt
negint: pydantic.NegativeInt
posfloat: pydantic.PositiveFloat
negfloat: pydantic.NegativeFloat
nonposint: pydantic.NonPositiveInt
nonnegint: pydantic.NonNegativeInt
nonposfloat: pydantic.NonPositiveFloat
nonnegfloat: pydantic.NonNegativeFloat
class JsonModel(pydantic.BaseModel):
json_any: pydantic.Json
json_int: pydantic.Json[int]
json_float: pydantic.Json[float]
json_str: pydantic.Json[str]
json_int_or_str: pydantic.Json[typing.Union[int, str]]
json_list_of_float: pydantic.Json[typing.List[float]]
class ConstrainedNumbersModel(pydantic.BaseModel):
conintt: pydantic.conint(gt=10, lt=100)
coninte: pydantic.conint(ge=10, le=100)
conintmul: pydantic.conint(ge=10, le=100, multiple_of=7)
confloatt: pydantic.confloat(gt=10, lt=100)
confloate: pydantic.confloat(ge=10, le=100)
condecimalt: pydantic.condecimal(gt=10, lt=100)
condecimale: pydantic.condecimal(ge=10, le=100)
yield from (
MiscModel,
StringsModel,
UUIDsModel,
IPvAnyAddress,
IPvAnyInterface,
IPvAnyNetwork,
StrictNumbersModel,
NumbersModel,
JsonModel,
ConstrainedNumbersModel,
)
try:
import_email_validator()
except ImportError:
pass
else:
class EmailsModel(pydantic.BaseModel):
email: pydantic.EmailStr
name_email: pydantic.NameEmail
yield EmailsModel
@pytest.mark.parametrize('model', gen_models())
@given(data=st.data())
def test_can_construct_models_with_all_fields(data, model):
# The value of this test is to confirm that Hypothesis knows how to provide
# valid values for each field - otherwise, this would raise ValidationError.
instance = data.draw(st.from_type(model))
# We additionally check that the instance really is of type `model`, because
# an evil implementation could avoid ValidationError by means of e.g.
# `st.register_type_strategy(model, st.none())`, skipping the constructor.
assert isinstance(instance, model)