Skip to content

Commit 9bbb00a

Browse files
committed
chore: Improve the test coverage. Add error handling tests
1 parent bbfb62a commit 9bbb00a

8 files changed

Lines changed: 197 additions & 6 deletions

File tree

sqlalchemy_serializer/lib/schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def to_strict(self):
3535
continue # Exclude leafs
3636
tree.to_strict()
3737

38-
def __repr__(self):
38+
def __repr__(self) -> str:
3939
include = f"to_include={self.to_include}"
4040
exclude = f"to_exclude={self.to_exclude}"
4141
greedy = f"is_greedy={self.is_greedy}"
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from abc import abstractmethod
2+
3+
14
class Base:
2-
def __call__(self, value) -> str:
3-
raise NotImplementedError(f"Method should implement serialization logic for {value}")
5+
@abstractmethod
6+
def __call__(self, value) -> str: ...

sqlalchemy_serializer/lib/serializable/time.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ class Time(Base):
88
def __init__(self, str_format: str = "%H:%M:%S") -> None:
99
self.str_format = str_format
1010

11-
def __call__(self, value: datetime.time):
11+
def __call__(self, value: datetime.time) -> str:
1212
return format_dt(tpl=self.str_format, dt=value)

sqlalchemy_serializer/lib/serializable/uuid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55

66
class UUID(Base):
7-
def __call__(self, value: uuid.UUID):
7+
def __call__(self, value: uuid.UUID) -> str:
88
return str(value)

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from .models import Base
1111

1212
logger = logging.getLogger("serializer")
13-
logger.setLevel(logging.DEBUG)
13+
logger.setLevel(logging.INFO)
1414

1515

1616
DEFAULT_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"

tests/test_error_handling.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Error handling tests for SQLAlchemy serializer."""
2+
3+
import pytest
4+
import sqlalchemy as sa
5+
6+
from sqlalchemy_serializer import SerializerMixin
7+
8+
from .models import Base
9+
10+
11+
class ModelWithFailingProperty(Base, SerializerMixin):
12+
"""Model with a property that raises an exception."""
13+
14+
__tablename__ = "model_with_failing_property"
15+
serialize_only = ()
16+
serialize_rules = ()
17+
18+
id = sa.Column(sa.Integer, primary_key=True)
19+
string = sa.Column(sa.String(256), default="test")
20+
21+
@property
22+
def failing_prop(self):
23+
"""Property that raises an exception."""
24+
raise ValueError("Property access failed")
25+
26+
@property
27+
def valid_prop(self):
28+
"""Property that works correctly."""
29+
return "valid value"
30+
31+
32+
class ModelWithFailingMethod(Base, SerializerMixin):
33+
"""Model with a method that raises an exception."""
34+
35+
__tablename__ = "model_with_failing_method"
36+
serialize_only = ()
37+
serialize_rules = ()
38+
39+
id = sa.Column(sa.Integer, primary_key=True)
40+
string = sa.Column(sa.String(256), default="test")
41+
42+
def failing_method(self):
43+
"""Method that raises an exception."""
44+
raise RuntimeError("Method call failed")
45+
46+
def valid_method(self):
47+
"""Method that works correctly."""
48+
return "valid result"
49+
50+
51+
class ModelWithFailingCustomSerializer(Base, SerializerMixin):
52+
"""Model with custom serializer that raises an exception."""
53+
54+
__tablename__ = "model_with_failing_custom_serializer"
55+
serialize_only = ()
56+
serialize_rules = ()
57+
58+
id = sa.Column(sa.Integer, primary_key=True)
59+
string = sa.Column(sa.String(256), default="test")
60+
number = sa.Column(sa.Integer, default=42)
61+
62+
63+
def test_property_raises_exception_default_behavior(get_instance):
64+
model = get_instance(ModelWithFailingProperty)
65+
66+
with pytest.raises(ValueError, match="Property access failed"):
67+
model.to_dict(rules=("failing_prop",))
68+
69+
70+
def test_method_raises_exception_default_behavior(get_instance):
71+
model = get_instance(ModelWithFailingMethod)
72+
73+
with pytest.raises(RuntimeError, match="Method call failed"):
74+
model.to_dict(rules=("failing_method",))
75+
76+
77+
def test_custom_serializer_raises_exception_default_behavior(get_instance):
78+
model = get_instance(ModelWithFailingCustomSerializer)
79+
80+
def failing_serializer(_value):
81+
"""Custom serializer that raises an exception."""
82+
raise TypeError("Custom serializer failed")
83+
84+
with pytest.raises(TypeError, match="Custom serializer failed"):
85+
model.to_dict(serialize_columns={"string": failing_serializer})

tests/test_is_valid_callable.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Tests for Serializer.is_valid_callable static method."""
2+
3+
import inspect
4+
from types import MethodType
5+
6+
import sqlalchemy as sa
7+
8+
from sqlalchemy_serializer import SerializerMixin
9+
from sqlalchemy_serializer.serializer import Serializer
10+
11+
from .models import Base
12+
13+
14+
def test_is_valid_callable_with_varargs():
15+
"""Test that functions with *args are not valid callables."""
16+
17+
def func_with_args(*args):
18+
return args
19+
20+
assert Serializer.is_valid_callable(func_with_args) is False
21+
22+
23+
def test_is_valid_callable_with_varkw():
24+
"""Test that functions with **kwargs are not valid callables."""
25+
26+
def func_with_kwargs(**kwargs):
27+
return kwargs
28+
29+
assert Serializer.is_valid_callable(func_with_kwargs) is False
30+
31+
32+
def test_is_valid_callable_with_both_varargs_and_varkw():
33+
"""Test that functions with both *args and **kwargs are not valid callables."""
34+
35+
def func_with_both(*args, **kwargs):
36+
return args, kwargs
37+
38+
assert Serializer.is_valid_callable(func_with_both) is False
39+
40+
41+
def test_is_valid_callable_method_without_self():
42+
"""Test that MethodType without 'self' as first arg is not valid callable."""
43+
44+
class ModelWithClassMethod(Base, SerializerMixin):
45+
__tablename__ = "model_with_classmethod"
46+
serialize_only = ()
47+
serialize_rules = ()
48+
49+
id = sa.Column(sa.Integer, primary_key=True)
50+
string = sa.Column(sa.String(256), default="test")
51+
52+
@classmethod
53+
def class_method(cls) -> str:
54+
return "result"
55+
56+
model = ModelWithClassMethod(id=1, string="test")
57+
method = model.class_method
58+
59+
# Verify it's a MethodType
60+
assert isinstance(method, MethodType)
61+
62+
# Should return False because first arg is 'cls', not 'self'
63+
assert Serializer.is_valid_callable(method) is False
64+
65+
66+
def test_is_valid_callable_method_with_empty_args():
67+
"""Test that MethodType with no args (not even self) is not valid callable."""
68+
69+
# Create a function with no args
70+
def func_no_args():
71+
return "result"
72+
73+
# Manually create a MethodType bound to an object
74+
# This simulates a method with no args
75+
# (which shouldn't happen in normal Python, but tests the edge case)
76+
class DummyObj:
77+
pass
78+
79+
obj = DummyObj()
80+
# Bind the function as a method
81+
method = MethodType(func_no_args, obj)
82+
83+
# Verify it's a MethodType
84+
assert isinstance(method, MethodType)
85+
86+
# Verify it has no args
87+
# inspect.getfullargspec on bound method returns underlying function's args
88+
spec = inspect.getfullargspec(method)
89+
assert not spec.args
90+
91+
# Should return False because no args
92+
assert Serializer.is_valid_callable(method) is False

tests/test_tree.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,14 @@ def test_merge_trees():
124124
assert leaf.is_greedy
125125
assert leaf.to_include
126126
assert leaf.to_exclude is None
127+
128+
129+
def test_tree_repr():
130+
tree = Tree(to_include=True, to_exclude=False, is_greedy=True)
131+
tree["key1"] = Tree(to_include=True, to_exclude=False, is_greedy=True)
132+
tree["key1"]["key2"] = Tree(to_include=True, to_exclude=False, is_greedy=True)
133+
assert repr(tree) == (
134+
"Tree(to_include=True, to_exclude=False, is_greedy=True)[\n"
135+
"key1: Tree(to_include=True, to_exclude=False, is_greedy=True)[\n"
136+
" key2: Tree(to_include=True, to_exclude=False, is_greedy=True)[]]]"
137+
)

0 commit comments

Comments
 (0)