Skip to content

Commit

Permalink
✨ feat: implement value object simple class and its unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
adriamontoto committed Jan 3, 2025
1 parent 1d26a9a commit d299096
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 0 deletions.
Empty file added tests/models/__init__.py
Empty file.
48 changes: 48 additions & 0 deletions tests/models/test_value_object_get_attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
Test value object module.
"""

from object_mother_pattern.mothers import IntegerMother
from pytest import mark, raises as assert_raises

from value_object_pattern import ValueObject


class IntegerValueObject(ValueObject[int]):
"""
IntegerValueObject value object class.
"""


@mark.unit_testing
def test_value_object_get_attribute() -> None:
"""
Test that a value object value can be accessed.
"""
value_object = IntegerValueObject(value=IntegerMother.create())

value_object.value # noqa: B018


@mark.unit_testing
def test_value_object_get_protected_attribute() -> None:
"""
Test that a value object protected value can be accessed.
"""
value_object = IntegerValueObject(value=IntegerMother.create())

value_object._value # noqa: B018


@mark.unit_testing
def test_value_object_cannot_get_unexistent_attribute() -> None:
"""
Test that a value object value cannot be modified after initialization.
"""
value_object = IntegerValueObject(value=IntegerMother.create())

with assert_raises(
expected_exception=AttributeError,
match=f"'{value_object.__class__.__name__}' object has no attribute 'not_existent_attribute'",
):
value_object.not_existent_attribute # type: ignore[attr-defined] # noqa: B018
56 changes: 56 additions & 0 deletions tests/models/test_value_object_set_attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Test value object module.
"""

from object_mother_pattern.mothers import IntegerMother
from pytest import mark, raises as assert_raises

from value_object_pattern import ValueObject


class IntegerValueObject(ValueObject[int]):
"""
IntegerValueObject value object class.
"""


@mark.unit_testing
def test_value_object_cannot_modify_value() -> None:
"""
Test that a value object value cannot be modified after initialization.
"""
value_object = IntegerValueObject(value=IntegerMother.create())

with assert_raises(
expected_exception=AttributeError,
match='Cannot modify attribute "value" of immutable instance',
):
value_object.value = IntegerMother.create() # type: ignore[misc]


@mark.unit_testing
def test_value_object_cannot_modify_protected_value() -> None:
"""
Test that a value object protected value cannot be modified after initialization.
"""
value_object = IntegerValueObject(value=IntegerMother.create())

with assert_raises(
expected_exception=AttributeError,
match='Cannot modify attribute "_value" of immutable instance',
):
value_object._value = IntegerMother.create()


@mark.unit_testing
def test_value_object_cannot_add_new_attribute() -> None:
"""
Test that cannot add a new attribute to a value object after initialization.
"""
value_object = IntegerValueObject(value=IntegerMother.create())

with assert_raises(
expected_exception=AttributeError,
match=f'{value_object.__class__.__name__} object has no attribute "new_attribute"',
):
value_object.new_attribute = IntegerMother.create()
4 changes: 4 additions & 0 deletions value_object_pattern/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
__version__ = '2025.01.02'

from .models import ValueObject

__all__ = ('ValueObject',)
3 changes: 3 additions & 0 deletions value_object_pattern/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .value_object import ValueObject

__all__ = ('ValueObject',)
108 changes: 108 additions & 0 deletions value_object_pattern/models/value_object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
Value object generic type.
"""

from abc import ABC
from typing import Generic, NoReturn, TypeVar, override

T = TypeVar('T')


class ValueObject(ABC, Generic[T]):
"""
ValueObject generic type.
"""

__slots__ = ('_value',)
__match_args__ = ('_value',)

_value: T

def __init__(self, *, value: T) -> None:
"""
ValueObject value object constructor.
Args:
value (T): Value.
"""
object.__setattr__(self, '_value', value)

@override
def __repr__(self) -> str:
"""
Returns a detailed string representation of the value object.
Returns:
str: A string representation of the value object in the format 'ClassName(value=value)'.
"""
return f'{self.__class__.__name__}(value={self._value!s})'

@override
def __str__(self) -> str:
"""
Returns a simple string representation of the value object.
Returns:
str: The string representation of the value object value.
"""
return str(object=self._value)

@override
def __hash__(self) -> int:
"""
Returns the hash of the value object.
Returns:
int: Hash of the value object.
"""
return hash(self._value)

@override
def __eq__(self, other: object) -> bool:
"""
Check if the value object is equal to another value object.
Args:
other (object): Object to compare.
Returns:
bool: True if both objects are equal, otherwise False.
"""
if not isinstance(other, self.__class__):
return NotImplemented

return self._value == other.value

@override
def __setattr__(self, key: str, value: T) -> NoReturn:
"""
Prevents modification or addition of attributes in the value object.
Args:
key (str): The name of the attribute.
value (T): The value to be assigned to the attribute.
Raises:
AttributeError: If there is an attempt to modify an existing attribute.
AttributeError: If there is an attempt to add a new attribute.
"""
public_key = key.replace('_', '')
public_slots1 = [slot.replace('_', '') for slot in self.__slots__]

if key in self.__slots__:
raise AttributeError(f'Cannot modify attribute "{key}" of immutable instance.')

if public_key in public_slots1:
raise AttributeError(f'Cannot modify attribute "{public_key}" of immutable instance.')

raise AttributeError(f'{self.__class__.__name__} object has no attribute "{key}".')

@property
def value(self) -> T:
"""
Returns the value object value.
Returns:
T: The value object value.
"""
return self._value

0 comments on commit d299096

Please sign in to comment.