Skip to main content

Declarative test data generation for Python

Project description

conformly

Python Versions License: MIT

Declarative test data generator for Python. Turns data models (now only dataclasses) and type constraints into valid fixtures and negative test cases.

conformly allows you to define your data schema once using standard Python dataclasses and Annotated constraints, and instantly generate rigorous test data. It replaces verbose factory patterns with a smart, schema-aware generator that supports both happy-path and edge-case testing.

Instead of writing separate factory classes or hardcoding test dictionaries, Conformly:

  • Extracts constraints from your dataclasses (length bounds, regex patterns, numeric ranges)
  • Generates valid data strictly adhering to all constraints for happy-path testing
  • Generates invalid data intelligently violating constraints for negative testing and fuzzing
  • Bridges static typing and dynamic testing — your schema is the single source of truth

Key Features

  • Zero Boilerplate: Works directly with standard dataclasses. No need to learn a new DSL.
  • Constraint-Aware: Respects min_length, max_length, pattern (regex), and numeric bounds (gt, ge, lt, le).
  • Negative Testing Built-in: Generates edge cases and boundary violations for robust error handling tests.
  • Flexible Definitions: Supports constraints via Annotated (explicit or shorthand) and field(metadata=...).
  • Pure Python: Lightweight, no heavy dependencies, works with standard library tools.

Install

pip install conformly
# or with uv
uv add conformly

Quickstart

Define a model:

from dataclasses import dataclass, field
from typing import Annotated
from conformly import case, cases


@dataclass
class User:
    username: Annotated[str, "min_length=3"]
    email: Annotated[str, "patter"=r"^[^\s@]+@[^\s@]+\.[^\s@]+$"]
    age: Annotated[int, "ge=18", "le=120"]

Generate valid data:

user = case(User, valid=True)
# -> {"username": "Abc", "email": "x@y.z", "age": 42}

Generate an invalid case for a specific field:

bad_user = case(User, valid=False, strategy="age")
# bad_user["age"] is outside 18..120 (either < 18 or > 120)

Generate many cases:

items = cases(User, valid=True, count=10)

Use Cases

case(Model, ...) # single generated object
cases(Model, ...) # list of generated objects

API Testing

# Valid payloads for happy-path tests
for _ in range(100):
    payload = case(CreateUserRequest, valid=True)
    response = client.post("/users", json=payload)
    assert response.status_code == 201

# Invalid payloads for error handling tests
invalid = case(CreateUserRequest, valid=False, strategy="age")
response = client.post("/users", json=invalid)
assert response.status_code == 400

Database Seeding

# Generate realistic test data respecting schema constraints
products = cases(Product, valid=True, count=1000)
db.insert_many("products", products)

Fuzzing & Property-Based Testing

Conformly is not a replacement of Hypotesis, but a complementary tool for schema-driven testing and negative case generation.

# Generate random invalid data to stress-test validation
for _ in range(500):
    invalid = case(Model, valid=False, strategy="random")
    assert validate(invalid) is False  # Should always reject

Supported Constraints

String

  • min_length — minimum string length
  • max_length — maximum string length
  • pattern — regex pattern (must match)

Integer / Float Bounds

  • gt — strictly greater than
  • ge — greater than or equal
  • lt — strictly less than
  • le — less than or equal

Boolean

  • Basic boolean generation (no extra constraints)

Defining Constraints

1) Annotated[..., ConstraintSpec(...)] (explicit)

from typing import Annotated
from conformly.specs import ConstraintSpec

username: Annotated[str, ConstraintSpec("min_length", 3)]
age: Annotated[int, ConstraintSpec("ge", 18)]

2) Annotated[..., "k=v"] (shorthand)

title: Annotated[str, "min_length=5", "max_length=200"]
views: Annotated[int, "ge=0"]
rating: Annotated[float, "ge=0", "le=5"]

3) field(metadata={...})

from dataclasses import field

sku: str = field(metadata={"pattern": r"^[A-Z0-9]{8}$"})
stock: int = field(metadata={"ge": 0})
price: float = field(metadata={"gt": 0})

Invalid Generation Contract (Important)

For case(Model, valid=False, strategy="<field>"):

  • Exactly one field is targeted (the one specified by strategy).
  • The generator will violate constraints for that field, making it invalid.
  • If a field has multiple constraints, the violated constraint may be chosen by generator logic (not necessarily the one you expect).
  • For numeric bounds, invalid values may violate the lower or upper bound (e.g., age > 120 or age < 18).
  • For float bounds, invalid generation may produce inf when violating the upper boundary.

If you need deterministic control over which exact constraint to violate, that is not implemented in 0.0.1 (see Roadmap).

Optional Fields and Defaults

  • If a field is optional (Optional[T]), valid generation may produce None.
  • If a field has a default value, valid generation returns the default.
  • Invalid generation requires at least one constraint on the targeted field (raises ValueError otherwise).

Development

Install dependencies:

uv sync

Run tests:

uv run -m pytest -q

Run with coverage:

uv run -m pytest --cov=conformly --cov-report=term-missing

Build & check package:

uv build
uv run -m twine check dist/*

Roadmap

  • Nested data models
  • Deterministic invalid generation - explicitly select which constraint to violate
  • Better regex invalidation - guarantee that invalid strings don't match patterns
  • More adapters - pydantic, TypedDict, attrs support
  • More constraints and types - multitiple_of, Literal, list[T], dict[T] etc.
  • Custom generators - allow per-field generator overrides

License

MIT — see LICENSE file for details

Contributing

Contributions welcome! Please:

  • Fork the repo
  • Create a feature branch
  • Add tests for new functionality
  • Run uv run -m pytest and uv run -m ruff check .
  • Submit a pull request

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

conformly-0.1.0.tar.gz (15.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

conformly-0.1.0-py3-none-any.whl (16.8 kB view details)

Uploaded Python 3

File details

Details for the file conformly-0.1.0.tar.gz.

File metadata

  • Download URL: conformly-0.1.0.tar.gz
  • Upload date:
  • Size: 15.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for conformly-0.1.0.tar.gz
Algorithm Hash digest
SHA256 fd7134d5a4176284d5360cc156d180a44ccce07bebd7414d770bcf9831e96da8
MD5 fcfedc03f335fc4a573789014a3c90b9
BLAKE2b-256 4cf30bcd3164be413fa05b52623f02235c12903026bc5915e57e48ec63f35d40

See more details on using hashes here.

File details

Details for the file conformly-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: conformly-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 16.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for conformly-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a0df67daf7403e88dfc1f0d9c05d9d8d9bd8acfe1cc580f36cb9decdd0edca27
MD5 492be1249ce1ca5d9bc0dd2501896d7d
BLAKE2b-256 8adabedd927c6dc355ad68fe1c00203db2ac48ee11df0a4738bd64a8d0f012f4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page