Skip to content

Commit dd9be32

Browse files
easelclaude
andcommitted
Add property-based tests using shared Hypothesis strategies
Use umf_dict() and umf_object() strategies from tests/strategies.py in: - Schema generators: SQL DDL, PySpark, JSON Schema never crash on valid input - UMF models: model_dump/reconstruct roundtrip preserves all fields - Compatibility: reflexivity (any UMF compatible with itself) Closes tablespec-e89 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ae4028e commit dd9be32

3 files changed

Lines changed: 147 additions & 2 deletions

File tree

tests/unit/test_compatibility.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
is_precision_compatible,
1919
is_safe_widening,
2020
)
21+
from tests.strategies import umf_object as shared_umf_object
2122

22-
pytestmark = [pytest.mark.no_spark, pytest.mark.fast]
23+
pytestmark = pytest.mark.no_spark
2324

2425

2526
# ---------------------------------------------------------------------------
@@ -406,3 +407,27 @@ def test_remove_any_column_always_breaking(self, umf):
406407
report = check_compatibility(umf, new)
407408
assert not report.is_backward_compatible
408409
assert any(i.change == "removed" for i in report.issues)
410+
411+
412+
class TestSharedStrategyProperties:
413+
"""Property-based tests using the shared umf_object strategy from tests.strategies."""
414+
415+
@given(umf=shared_umf_object())
416+
@settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow])
417+
def test_reflexivity_with_shared_strategy(self, umf):
418+
"""check_compatibility(umf, umf) reports no breaking issues for any UMF."""
419+
report = check_compatibility(umf, umf)
420+
assert report.is_backward_compatible
421+
assert report.is_forward_compatible
422+
assert len(report.issues) == 0
423+
class TestSharedStrategyProperties:
424+
"""Property-based tests using the shared umf_object strategy from tests.strategies."""
425+
426+
@given(umf=shared_umf_object())
427+
@settings(max_examples=50, suppress_health_check=[HealthCheck.too_slow])
428+
def test_reflexivity_with_shared_strategy(self, umf):
429+
"""check_compatibility(umf, umf) reports no breaking issues for any UMF."""
430+
report = check_compatibility(umf, umf)
431+
assert report.is_backward_compatible
432+
assert report.is_forward_compatible
433+
assert len(report.issues) == 0

tests/unit/test_schema_generators.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import json
66

77
import pytest
8+
from hypothesis import given, settings
89

910
from tablespec.schemas.generators import (
1011
generate_json_schema,
1112
generate_pyspark_schema,
1213
generate_sql_ddl,
1314
)
15+
from tests.strategies import umf_dict
1416

1517
pytestmark = pytest.mark.no_spark
1618

@@ -464,3 +466,71 @@ def test_empty_required_when_all_nullable(self):
464466

465467
schema = generate_json_schema(umf)
466468
assert schema["required"] == []
469+
470+
471+
class TestPropertyBasedGenerators:
472+
"""Property-based tests for schema generators using Hypothesis."""
473+
474+
@given(data=umf_dict())
475+
@settings(max_examples=50, deadline=None)
476+
def test_sql_ddl_never_crashes_and_returns_nonempty(self, data):
477+
"""Any valid UMF dict produces non-empty SQL DDL without crashing."""
478+
result = generate_sql_ddl(data)
479+
assert isinstance(result, str)
480+
assert len(result) > 0
481+
assert "CREATE TABLE" in result
482+
483+
@given(data=umf_dict())
484+
@settings(max_examples=50, deadline=None)
485+
def test_pyspark_schema_never_crashes_and_returns_nonempty(self, data):
486+
"""Any valid UMF dict produces non-empty PySpark schema without crashing."""
487+
result = generate_pyspark_schema(data)
488+
assert isinstance(result, str)
489+
assert len(result) > 0
490+
assert "StructType" in result
491+
492+
@given(data=umf_dict())
493+
@settings(max_examples=50, deadline=None)
494+
def test_json_schema_never_crashes_and_returns_valid_json(self, data):
495+
"""Any valid UMF dict produces valid JSON Schema that serializes correctly."""
496+
result = generate_json_schema(data)
497+
assert isinstance(result, dict)
498+
assert "properties" in result
499+
assert len(result["properties"]) > 0
500+
# Must be JSON-serializable
501+
json_str = json.dumps(result)
502+
parsed = json.loads(json_str)
503+
assert parsed == result
504+
class TestPropertyBasedGenerators:
505+
"""Property-based tests for schema generators using Hypothesis."""
506+
507+
@given(data=umf_dict())
508+
@settings(max_examples=50, deadline=None)
509+
def test_sql_ddl_never_crashes_and_returns_nonempty(self, data):
510+
"""Any valid UMF dict produces non-empty SQL DDL without crashing."""
511+
result = generate_sql_ddl(data)
512+
assert isinstance(result, str)
513+
assert len(result) > 0
514+
assert "CREATE TABLE" in result
515+
516+
@given(data=umf_dict())
517+
@settings(max_examples=50, deadline=None)
518+
def test_pyspark_schema_never_crashes_and_returns_nonempty(self, data):
519+
"""Any valid UMF dict produces non-empty PySpark schema without crashing."""
520+
result = generate_pyspark_schema(data)
521+
assert isinstance(result, str)
522+
assert len(result) > 0
523+
assert "StructType" in result
524+
525+
@given(data=umf_dict())
526+
@settings(max_examples=50, deadline=None)
527+
def test_json_schema_never_crashes_and_returns_valid_json(self, data):
528+
"""Any valid UMF dict produces valid JSON Schema that serializes correctly."""
529+
result = generate_json_schema(data)
530+
assert isinstance(result, dict)
531+
assert "properties" in result
532+
assert len(result["properties"]) > 0
533+
# Must be JSON-serializable
534+
json_str = json.dumps(result)
535+
parsed = json.loads(json_str)
536+
assert parsed == result

tests/unit/test_umf_models.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pydantic import ValidationError
66
import pytest
7+
from hypothesis import given, settings
78

89
from tablespec.models.umf import (
910
UMF,
@@ -14,8 +15,9 @@
1415
UMFColumn,
1516
UMFColumnDerivation,
1617
)
18+
from tests.strategies import umf_object
1719

18-
pytestmark = [pytest.mark.no_spark, pytest.mark.fast]
20+
pytestmark = pytest.mark.no_spark
1921

2022

2123
class TestNullable:
@@ -668,3 +670,51 @@ def test_dict_exclude_none(self, minimal_umf_data):
668670
assert "validation_rules" not in data
669671

670672

673+
class TestPropertyBasedUMF:
674+
"""Property-based tests for UMF model roundtrip."""
675+
676+
@given(umf=umf_object())
677+
@settings(max_examples=50, deadline=None)
678+
def test_roundtrip_model_dump_and_reconstruct(self, umf):
679+
"""Any UMF from umf_object() round-trips through model_dump/reconstruct."""
680+
data = umf.model_dump()
681+
reconstructed = UMF(**data)
682+
683+
assert reconstructed.version == umf.version
684+
assert reconstructed.table_name == umf.table_name
685+
assert len(reconstructed.columns) == len(umf.columns)
686+
687+
for orig_col, new_col in zip(umf.columns, reconstructed.columns):
688+
assert new_col.name == orig_col.name
689+
assert new_col.data_type == orig_col.data_type
690+
assert new_col.length == orig_col.length
691+
assert new_col.precision == orig_col.precision
692+
assert new_col.scale == orig_col.scale
693+
assert new_col.nullable == orig_col.nullable
694+
assert new_col.description == orig_col.description
695+
696+
697+
class TestPropertyBasedUMF:
698+
"""Property-based tests for UMF model roundtrip."""
699+
700+
@given(umf=umf_object())
701+
@settings(max_examples=50, deadline=None)
702+
def test_roundtrip_model_dump_and_reconstruct(self, umf):
703+
"""Any UMF from umf_object() round-trips through model_dump/reconstruct."""
704+
data = umf.model_dump()
705+
reconstructed = UMF(**data)
706+
707+
assert reconstructed.version == umf.version
708+
assert reconstructed.table_name == umf.table_name
709+
assert len(reconstructed.columns) == len(umf.columns)
710+
711+
for orig_col, new_col in zip(umf.columns, reconstructed.columns):
712+
assert new_col.name == orig_col.name
713+
assert new_col.data_type == orig_col.data_type
714+
assert new_col.length == orig_col.length
715+
assert new_col.precision == orig_col.precision
716+
assert new_col.scale == orig_col.scale
717+
assert new_col.nullable == orig_col.nullable
718+
assert new_col.description == orig_col.description
719+
720+

0 commit comments

Comments
 (0)