Generative (property) testing, from python annotations.
Inspired by QuickCheck, similar goals to hypothesis, but focused on annotations doing the heavy lifting.
Enable the plugin in your pytest configuration, then
use @parametrize
on a test function and get things.
from johen.pytest import parametrize
@parametrize
def test_thing(my_model: Model, inputs: list[str]):
my_model.update(a=inputs)
assert my_model.a == inputs
Works on many common types of datatypes:
- Named tuples
- dataclasses
- pydantic v2 models (opt in)
- sqlalchemy models (opt in)
- tuples, lists, sets
- TypedDict
- primitives
- UUIDs
- easy to extend to support more types
Use parametrize
to configure options for specific test groups.
For instance, you can use arg_set
to distinguish generated arguments vs fixtures.
@parametrize(count=15, seed=2, arg_set=('a',))
def my_test(a: int, unrelated_fixture):
pass
Use replace_global_config
to conditionally update global options, or write
directly to global_config
if you intend to persist your config adjustments.
global_config.matches.append(my_custom_matcher)
@pytest.fixture(autouse=True)
def setup_config():
with replace_global_config(dict(max_iterations=500)):
yield
You have two main strategies.
The easiest is to add the type to "type_matchers"
of the global_config.
You can use python generator expressions in combination with gen
to create deterministic generative methods.
from johen import global_config, gen
global_config["type_matchers"][MyType] = (MyType(r.randint(0, 10)) for r in gen)
The other is to add a custom AnnotationMatcher
to the "matchers"
key. See generates/base.py for examples.
It is possible to support recursive types through forward references, but do note that
their generation is typically expensive. Use the "globals"
key of the global_config
to configure the forward refs ahead of time:
from johen.specialized import JsonDict, JsonValue
global_config['globals'].extend({
"JsonDict": JsonDict,
"JsonValue": JsonValue,
})