Добавлены тесты для Environment
This commit is contained in:
@@ -8,4 +8,8 @@
|
||||
Пакет описания тестов для служебных классов
|
||||
"""
|
||||
|
||||
from .date_utils_now_tests import DateUtilsNowTests
|
||||
from .date_utils_shift_time_tests import DateUtilsShiftTimeTests
|
||||
from .model_utils_concat_data_tests import ModelUtilsConcatDataTests
|
||||
from .model_utils_update_data_tests import ModelUtilsUpdateDataTests
|
||||
from .settings import *
|
||||
|
||||
103
net/xeaf/rack/tests/utils/_model_utils_mocks.py
Normal file
103
net/xeaf/rack/tests/utils/_model_utils_mocks.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание mock-классов для тестов ModelUtils
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Model
|
||||
|
||||
|
||||
class TestSampleModel(Model):
|
||||
"""
|
||||
Вспомогательный mock-класс модели для тестирования методов ModelUtils.
|
||||
Предназначен для сценариев, где data является экземпляром модели.
|
||||
Имитирует базовую модель Django с полями id, name и description.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
app_label = "test_app"
|
||||
managed = False
|
||||
|
||||
id = models.IntegerField(primary_key=True, default=1)
|
||||
name = models.CharField(max_length=100, default="test_name")
|
||||
description = models.CharField(max_length=255, default="test_description")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class TestEmptyModel(Model):
|
||||
"""
|
||||
Вспомогательный mock-класс пустой модели для тестирования граничных случаев.
|
||||
Предназначен для сценариев, где модель не содержит полей.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
app_label = "test_app"
|
||||
managed = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class TestModelWithExtraFields(Model):
|
||||
"""
|
||||
Вспомогательный mock-класс модели с дополнительными полями.
|
||||
Предназначен для сценариев, где необходимо проверить работу с полями,
|
||||
которые могут отсутствовать в целевом словаре.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
app_label = "test_app"
|
||||
managed = False
|
||||
|
||||
id = models.IntegerField(primary_key=True, default=1)
|
||||
name = models.CharField(max_length=100, default="test_name")
|
||||
email = models.EmailField(max_length=255, default="test@example.com")
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class TestModelWithSpecialFields(Model):
|
||||
"""
|
||||
Вспомогательный mock-класс модели с полями специальных типов.
|
||||
Предназначен для проверки корректной обработки полей с None значениями
|
||||
и полей, которые могут быть не установлены.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
app_label = "test_app"
|
||||
managed = False
|
||||
|
||||
id = models.IntegerField(primary_key=True, default=1)
|
||||
nullable_field = models.CharField(max_length=100, null=True, blank=True, default=None)
|
||||
boolean_field = models.BooleanField(default=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class TestModelWithDifferentFieldTypes(Model):
|
||||
"""
|
||||
Вспомогательный mock-класс модели с полями различных типов.
|
||||
Предназначен для проверки корректной обработки разнотипных полей.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
app_label = "test_app"
|
||||
managed = False
|
||||
|
||||
id = models.IntegerField(primary_key=True, default=1)
|
||||
integer_field = models.IntegerField(default=0)
|
||||
float_field = models.FloatField(default=0.0)
|
||||
text_field = models.TextField(default="")
|
||||
boolean_field = models.BooleanField(default=False)
|
||||
date_field = models.DateField(null=True, blank=True)
|
||||
datetime_field = models.DateTimeField(null=True, blank=True)
|
||||
101
net/xeaf/rack/tests/utils/date_utils_now_tests.py
Normal file
101
net/xeaf/rack/tests/utils/date_utils_now_tests.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса DateUtilsNowTests
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.core.testing import CoreSimpleTestCase
|
||||
from net.xeaf.rack.utils import DateUtils
|
||||
|
||||
|
||||
class DateUtilsNowTests(CoreSimpleTestCase):
|
||||
"""
|
||||
Тестовый класс для DateUtils.now()
|
||||
"""
|
||||
|
||||
def test_now_returns_current_datetime(self):
|
||||
"""
|
||||
Проверяет, что метод now() возвращает текущую дату и время.
|
||||
Тестирует позитивный сценарий получения текущего времени.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
expected_now = datetime.datetime(2024, 1, 15, 12, 30, 45, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
with patch('django.utils.timezone.now', return_value=expected_now):
|
||||
result = DateUtils.now()
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_now)
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
|
||||
def test_now_returns_aware_datetime(self):
|
||||
"""
|
||||
Проверяет, что now() возвращает aware datetime (с часовым поясом).
|
||||
Тестирует, что возвращаемое время содержит информацию о часовом поясе.
|
||||
"""
|
||||
|
||||
# Arrange & Act
|
||||
result = DateUtils.now()
|
||||
|
||||
# Assert
|
||||
self.assertIsNotNone(result.tzinfo)
|
||||
self.assertTrue(timezone.is_aware(result))
|
||||
|
||||
def test_now_returns_correct_type(self):
|
||||
"""
|
||||
Проверяет, что now() возвращает объект типа datetime.datetime.
|
||||
Тестирует корректность типа возвращаемого значения.
|
||||
"""
|
||||
|
||||
# Arrange & Act
|
||||
result = DateUtils.now()
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(result, datetime.datetime)
|
||||
|
||||
def test_now_uses_django_timezone(self):
|
||||
"""
|
||||
Проверяет, что now() использует django.utils.timezone.now().
|
||||
Тестирует, что метод вызывает правильную функцию из Django.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
expected_time = datetime.datetime(2024, 6, 1, 10, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
with patch('django.utils.timezone.now', return_value=expected_time) as mock_now:
|
||||
result = DateUtils.now()
|
||||
|
||||
# Assert
|
||||
mock_now.assert_called_once()
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_now_returns_different_values_on_consecutive_calls(self):
|
||||
"""
|
||||
Проверяет, что при последовательных вызовах now() возвращаются разные значения.
|
||||
Тестирует, что метод действительно возвращает актуальное время.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
time1 = datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
time2 = datetime.datetime(2024, 1, 1, 0, 0, 1, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
with patch('django.utils.timezone.now', side_effect=[time1, time2]):
|
||||
result1 = DateUtils.now()
|
||||
result2 = DateUtils.now()
|
||||
|
||||
# Assert
|
||||
self.assertNotEqual(result1, result2)
|
||||
self.assertLess(result1, result2)
|
||||
306
net/xeaf/rack/tests/utils/date_utils_shift_time_tests.py
Normal file
306
net/xeaf/rack/tests/utils/date_utils_shift_time_tests.py
Normal file
@@ -0,0 +1,306 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса DateUtilsShiftTimeTests
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytz
|
||||
from django.utils import timezone
|
||||
|
||||
from net.xeaf.rack.core.testing import CoreSimpleTestCase
|
||||
from net.xeaf.rack.utils import DateUtils
|
||||
|
||||
|
||||
class DateUtilsShiftTimeTests(CoreSimpleTestCase):
|
||||
"""
|
||||
Тестовый класс для DateUtils.shift_time()
|
||||
"""
|
||||
|
||||
def test_shift_time_with_default_date_time(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с использованием текущей даты по умолчанию.
|
||||
Тестирует позитивный сценарий, когда date_time не передан.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 20, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
with patch('django.utils.timezone.now', return_value=base_time):
|
||||
result = DateUtils.shift_time(days=5)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_with_explicit_date_time(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с явно переданной датой.
|
||||
Тестирует позитивный сценарий с указанием конкретной даты.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 20, 10, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=5)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_with_positive_values(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с положительными значениями.
|
||||
Тестирует добавление дней, часов, минут и секунд.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 15, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 17, 15, 45, 30, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=2, hours=5, minutes=15, seconds=15)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_with_negative_values(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с отрицательными значениями.
|
||||
Тестирует вычитание дней, часов, минут и секунд.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 13, 5, 15, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=-2, hours=-5, minutes=-15)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_with_zero_values(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с нулевыми значениями.
|
||||
Тестирует граничный случай, когда время не изменяется.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=0, hours=0, minutes=0, seconds=0)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, base_time)
|
||||
|
||||
def test_shift_time_with_only_hours(self):
|
||||
"""
|
||||
Проверяет сдвиг времени только по часам.
|
||||
Тестирует добавление только часов без изменения других параметров.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 15, 15, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, hours=5)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_with_only_minutes(self):
|
||||
"""
|
||||
Проверяет сдвиг времени только по минутам.
|
||||
Тестирует добавление только минут без изменения других параметров.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 15, 10, 45, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, minutes=15)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_with_only_seconds(self):
|
||||
"""
|
||||
Проверяет сдвиг времени только по секундам.
|
||||
Тестирует добавление только секунд без изменения других параметров.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 15, 10, 30, 30, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, seconds=30)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_preserves_timezone(self):
|
||||
"""
|
||||
Проверяет, что сдвиг времени сохраняет часовой пояс.
|
||||
Тестирует корректность работы с aware datetime.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=1, hours=2)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result.tzinfo, base_time.tzinfo)
|
||||
self.assertTrue(timezone.is_aware(result))
|
||||
|
||||
def test_shift_time_crosses_day_boundary(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с переходом через границу дня.
|
||||
Тестирует корректность работы при добавлении часов, превышающих 24.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 23, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 17, 2, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, hours=27)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_crosses_month_boundary(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с переходом через границу месяца.
|
||||
Тестирует корректность работы при добавлении дней, превышающих количество дней в месяце.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 30, 10, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 2, 3, 10, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=4)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_crosses_year_boundary(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с переходом через границу года.
|
||||
Тестирует корректность работы при добавлении дней, превышающих количество дней в году.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 12, 30, 10, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2025, 1, 3, 10, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=4)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_with_large_values(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с большими значениями.
|
||||
Тестирует работу с большими числами дней, часов и минут.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 2, 15, 12, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=45, hours=12, minutes=30)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_with_none_date_time(self):
|
||||
"""
|
||||
Проверяет сдвиг времени при None в качестве date_time.
|
||||
Тестирует, что при None используется текущее время.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 8, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 20, 8, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
with patch('django.utils.timezone.now', return_value=base_time):
|
||||
result = DateUtils.shift_time(None, days=5)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_handles_daylight_saving(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с учетом перехода на летнее время.
|
||||
Тестирует корректность работы с DST (если применимо).
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
tz = pytz.timezone('Europe/Moscow')
|
||||
base_time = tz.localize(datetime.datetime(2024, 3, 30, 23, 0, 0))
|
||||
expected_time = tz.localize(datetime.datetime(2024, 3, 31, 23, 0, 0))
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(base_time, days=1)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
|
||||
def test_shift_time_returns_same_type_as_input(self):
|
||||
"""
|
||||
Проверяет, что возвращаемый тип соответствует входному.
|
||||
Тестирует, что метод возвращает datetime даже при None входном значении.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 0, 0, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result_with_date = DateUtils.shift_time(base_time, days=1)
|
||||
with patch('django.utils.timezone.now', return_value=base_time):
|
||||
result_without_date = DateUtils.shift_time(None, days=1)
|
||||
|
||||
# Assert
|
||||
self.assertIsInstance(result_with_date, datetime.datetime)
|
||||
self.assertIsInstance(result_without_date, datetime.datetime)
|
||||
|
||||
def test_shift_time_combination_of_all_parameters(self):
|
||||
"""
|
||||
Проверяет сдвиг времени с комбинацией всех параметров.
|
||||
Тестирует одновременное добавление дней, часов, минут и секунд.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
base_time = datetime.datetime(2024, 1, 15, 10, 30, 15, tzinfo=datetime.timezone.utc)
|
||||
expected_time = datetime.datetime(2024, 1, 25, 18, 45, 45, tzinfo=datetime.timezone.utc)
|
||||
|
||||
# Act
|
||||
result = DateUtils.shift_time(
|
||||
base_time,
|
||||
days=10,
|
||||
hours=8,
|
||||
minutes=15,
|
||||
seconds=30
|
||||
)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(result, expected_time)
|
||||
181
net/xeaf/rack/tests/utils/model_utils_concat_data_tests.py
Normal file
181
net/xeaf/rack/tests/utils/model_utils_concat_data_tests.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ModelUtilsConcatDataTests
|
||||
"""
|
||||
|
||||
from net.xeaf.rack.core.testing import CoreSimpleTestCase
|
||||
from net.xeaf.rack.utils import ModelUtils
|
||||
from ._model_utils_mocks import TestEmptyModel
|
||||
from ._model_utils_mocks import TestModelWithExtraFields
|
||||
from ._model_utils_mocks import TestModelWithSpecialFields
|
||||
from ._model_utils_mocks import TestSampleModel
|
||||
|
||||
|
||||
class ModelUtilsConcatDataTests(CoreSimpleTestCase):
|
||||
"""
|
||||
Тестовый класс для ModelUtils.concat_data()
|
||||
"""
|
||||
|
||||
def test_concat_data_with_dict_source_success(self):
|
||||
"""
|
||||
Проверяет успешное объединение данных из словаря-источника в целевой словарь.
|
||||
Тестирует позитивный сценарий, когда источник - обычный словарь.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
source_data = {"name": "John", "age": 30, "city": "Moscow"}
|
||||
target_data = {"name": "Existing", "country": "Russia"}
|
||||
|
||||
# Act
|
||||
ModelUtils.concat_data(source_data, target_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(target_data["name"], "Existing")
|
||||
self.assertEqual(target_data["age"], 30)
|
||||
self.assertEqual(target_data["city"], "Moscow")
|
||||
self.assertEqual(target_data["country"], "Russia")
|
||||
|
||||
def test_concat_data_with_model_source_success(self):
|
||||
"""
|
||||
Проверяет успешное объединение данных из модели-источника в целевой словарь.
|
||||
Тестирует позитивный сценарий, когда источник - экземпляр Model.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
model_instance = TestSampleModel(id=5, name="Alice", description="Test description")
|
||||
target_data = {"name": "Existing", "status": "active"}
|
||||
|
||||
# Act
|
||||
ModelUtils.concat_data(model_instance, target_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(target_data["name"], "Existing")
|
||||
self.assertEqual(target_data["id"], 5)
|
||||
self.assertEqual(target_data["description"], "Test description")
|
||||
self.assertEqual(target_data["status"], "active")
|
||||
|
||||
def test_concat_data_with_empty_model_source(self):
|
||||
"""
|
||||
Проверяет обработку пустой модели в качестве источника данных.
|
||||
Тестирует граничный случай, когда модель не имеет полей.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
model_instance = TestEmptyModel()
|
||||
target_data = {"name": "Existing", "status": "active"}
|
||||
|
||||
# Act
|
||||
ModelUtils.concat_data(model_instance, target_data)
|
||||
|
||||
# Assert
|
||||
# В пустой модели model_to_dict() вернет пустой словарь
|
||||
self.assertEqual(target_data["name"], "Existing")
|
||||
self.assertEqual(target_data["status"], "active")
|
||||
self.assertEqual(len(target_data), 2)
|
||||
|
||||
def test_concat_data_with_empty_source_dict(self):
|
||||
"""
|
||||
Проверяет обработку пустого словаря в качестве источника данных.
|
||||
Тестирует граничный случай, когда источник данных не содержит полей.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
source_data = {}
|
||||
target_data = {"name": "Existing", "status": "active"}
|
||||
|
||||
# Act
|
||||
ModelUtils.concat_data(source_data, target_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(target_data["name"], "Existing")
|
||||
self.assertEqual(target_data["status"], "active")
|
||||
self.assertEqual(len(target_data), 2)
|
||||
|
||||
def test_concat_data_does_not_override_existing_keys(self):
|
||||
"""
|
||||
Проверяет, что существующие ключи в целевом словаре не перезаписываются.
|
||||
Тестирует корректность логики пропуска уже существующих ключей.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
source_data = {"name": "John", "age": 30, "city": "Moscow"}
|
||||
target_data = {"name": "Existing", "age": 25, "country": "Russia"}
|
||||
|
||||
# Act
|
||||
ModelUtils.concat_data(source_data, target_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(target_data["name"], "Existing")
|
||||
self.assertEqual(target_data["age"], 25)
|
||||
self.assertEqual(target_data["city"], "Moscow")
|
||||
self.assertEqual(target_data["country"], "Russia")
|
||||
|
||||
def test_concat_data_with_model_containing_none_values(self):
|
||||
"""
|
||||
Проверяет обработку модели, содержащей поля со значением None.
|
||||
Тестирует, что None значения корректно копируются в целевой словарь.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
model_instance = TestModelWithSpecialFields(id=3, nullable_field=None, boolean_field=True)
|
||||
target_data = {"name": "Existing"}
|
||||
|
||||
# Act
|
||||
ModelUtils.concat_data(model_instance, target_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(target_data["name"], "Existing")
|
||||
self.assertEqual(target_data["id"], 3)
|
||||
self.assertIsNone(target_data["nullable_field"])
|
||||
self.assertTrue(target_data["boolean_field"])
|
||||
|
||||
def test_concat_data_with_complex_model_fields(self):
|
||||
"""
|
||||
Проверяет обработку модели с различными типами полей.
|
||||
Тестирует корректность копирования полей разных типов.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
model_instance = TestModelWithExtraFields(
|
||||
id=7,
|
||||
name="Bob",
|
||||
email="bob@example.com",
|
||||
is_active=False
|
||||
)
|
||||
target_data = {"status": "pending"}
|
||||
|
||||
# Act
|
||||
ModelUtils.concat_data(model_instance, target_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(target_data["status"], "pending")
|
||||
self.assertEqual(target_data["id"], 7)
|
||||
self.assertEqual(target_data["name"], "Bob")
|
||||
self.assertEqual(target_data["email"], "bob@example.com")
|
||||
self.assertFalse(target_data["is_active"])
|
||||
|
||||
def test_concat_data_handles_non_string_keys(self):
|
||||
"""
|
||||
Проверяет обработку словаря с не строковыми ключами.
|
||||
Тестирует, что метод корректно обрабатывает различные типы ключей.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
source_data = {1: "one", 2: "two", "key": "value"}
|
||||
target_data = {"name": "Existing"}
|
||||
|
||||
# Act
|
||||
ModelUtils.concat_data(source_data, target_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(target_data["name"], "Existing")
|
||||
# noinspection bad-index
|
||||
self.assertEqual(target_data[1], "one")
|
||||
# noinspection bad-index
|
||||
self.assertEqual(target_data[2], "two")
|
||||
self.assertEqual(target_data["key"], "value")
|
||||
229
net/xeaf/rack/tests/utils/model_utils_update_data_tests.py
Normal file
229
net/xeaf/rack/tests/utils/model_utils_update_data_tests.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса ModelUtilsUpdateDataTests
|
||||
"""
|
||||
|
||||
from net.xeaf.rack.core.testing import CoreSimpleTestCase
|
||||
from net.xeaf.rack.utils import ModelUtils
|
||||
from ._model_utils_mocks import TestModelWithExtraFields
|
||||
from ._model_utils_mocks import TestModelWithSpecialFields
|
||||
from ._model_utils_mocks import TestSampleModel
|
||||
|
||||
|
||||
class ModelUtilsUpdateDataTests(CoreSimpleTestCase):
|
||||
"""
|
||||
Тестовый класс для ModelUtils.update_data()
|
||||
"""
|
||||
|
||||
def test_update_data_with_model_instance_success(self):
|
||||
"""
|
||||
Проверяет успешное обновление атрибутов модели на основе переданных данных.
|
||||
Тестирует позитивный сценарий обновления экземпляра модели.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = TestSampleModel(id=1, name="Old Name", description="Old description")
|
||||
validated_data = {"name": "New Name", "description": "New description"}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance.name, "New Name")
|
||||
self.assertEqual(instance.description, "New description")
|
||||
self.assertEqual(instance.id, 1) # id не должен измениться
|
||||
|
||||
def test_update_data_with_dict_instance_success(self):
|
||||
"""
|
||||
Проверяет успешное обновление словаря-объекта на основе переданных данных.
|
||||
Тестирует позитивный сценарий обновления словаря.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = {"name": "Old Name", "age": 25, "city": "Moscow"}
|
||||
validated_data = {"name": "New Name", "age": 30}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance["name"], "New Name")
|
||||
self.assertEqual(instance["age"], 30)
|
||||
self.assertEqual(instance["city"], "Moscow")
|
||||
|
||||
def test_update_data_skips_nonexistent_attributes(self):
|
||||
"""
|
||||
Проверяет, что метод пропускает атрибуты, отсутствующие в объекте.
|
||||
Тестирует корректную обработку полей, которых нет в экземпляре.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = TestSampleModel(id=1, name="Old Name")
|
||||
validated_data = {"name": "New Name", "nonexistent_field": "Should be skipped"}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance.name, "New Name")
|
||||
self.assertFalse(hasattr(instance, "nonexistent_field"))
|
||||
self.assertEqual(instance.id, 1)
|
||||
|
||||
def test_update_data_with_empty_validated_data(self):
|
||||
"""
|
||||
Проверяет обработку пустого словаря данных для обновления.
|
||||
Тестирует граничный случай, когда нет данных для обновления.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = TestSampleModel(id=1, name="Original Name", description="Original description")
|
||||
validated_data = {}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance.name, "Original Name")
|
||||
self.assertEqual(instance.description, "Original description")
|
||||
self.assertEqual(instance.id, 1)
|
||||
|
||||
def test_update_data_with_dict_and_missing_keys(self):
|
||||
"""
|
||||
Проверяет обновление словаря с ключами, часть которых отсутствует.
|
||||
Тестирует обработку ситуации, когда ключ из validated_data отсутствует в словаре.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = {"name": "Old Name", "city": "Moscow"}
|
||||
validated_data = {"name": "New Name", "age": 30}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance["name"], "New Name")
|
||||
self.assertEqual(instance["city"], "Moscow")
|
||||
with self.assertRaises(KeyError):
|
||||
# Новый ключ не будет добавлен
|
||||
self.assertEqual(instance["age"], 30)
|
||||
|
||||
def test_update_data_partially_updates_model(self):
|
||||
"""
|
||||
Проверяет частичное обновление модели (не все поля переданы).
|
||||
Тестирует, что обновляются только переданные поля.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = TestSampleModel(id=1, name="Old Name", description="Old description")
|
||||
validated_data = {"name": "New Name"}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance.name, "New Name")
|
||||
self.assertEqual(instance.description, "Old description")
|
||||
self.assertEqual(instance.id, 1)
|
||||
|
||||
def test_update_data_with_model_containing_readonly_fields(self):
|
||||
"""
|
||||
Проверяет обновление модели с полями, которые могут быть защищены.
|
||||
Тестирует, что метод не пытается обновить несуществующие атрибуты.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = TestModelWithExtraFields(
|
||||
id=5,
|
||||
name="Old Name",
|
||||
email="old@example.com",
|
||||
is_active=True
|
||||
)
|
||||
validated_data = {"name": "New Name", "email": "new@example.com"}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance.name, "New Name")
|
||||
self.assertEqual(instance.email, "new@example.com")
|
||||
self.assertTrue(instance.is_active)
|
||||
self.assertEqual(instance.id, 5)
|
||||
|
||||
def test_update_data_with_invalid_data_types(self):
|
||||
"""
|
||||
Проверяет обработку некорректных типов данных.
|
||||
Тестирует, что метод корректно обрабатывает различные типы значений.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = TestSampleModel(id=1, name="Old Name", description="Old description")
|
||||
validated_data = {"name": 123, "description": None}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance.name, 123) # Любое значение будет присвоено
|
||||
self.assertIsNone(instance.description)
|
||||
self.assertEqual(instance.id, 1)
|
||||
|
||||
def test_update_data_with_nested_structures(self):
|
||||
"""
|
||||
Проверяет обновление с вложенными структурами данных.
|
||||
Тестирует обработку сложных типов данных.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = {"name": "Old", "data": {"key": "value"}}
|
||||
validated_data = {"name": "New", "data": {"new_key": "new_value"}}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(instance["name"], "New")
|
||||
self.assertEqual(instance["data"], {"new_key": "new_value"})
|
||||
|
||||
def test_update_data_with_bool_values(self):
|
||||
"""
|
||||
Проверяет обновление с булевыми значениями.
|
||||
Тестирует корректную обработку True/False значений.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = TestModelWithSpecialFields(id=1, nullable_field="test", boolean_field=False)
|
||||
validated_data = {"boolean_field": True, "nullable_field": None}
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertTrue(instance.boolean_field)
|
||||
self.assertIsNone(instance.nullable_field)
|
||||
self.assertEqual(instance.id, 1)
|
||||
|
||||
def test_update_data_does_not_modify_original_dict(self):
|
||||
"""
|
||||
Проверяет, что метод не создает копий и не изменяет исходные словари.
|
||||
Тестирует, что обновление происходит по ссылке.
|
||||
"""
|
||||
|
||||
# Arrange
|
||||
instance = {"name": "Old", "age": 20}
|
||||
validated_data = {"name": "New", "city": "Moscow"}
|
||||
original_instance_id = id(instance)
|
||||
|
||||
# Act
|
||||
ModelUtils.update_data(instance, validated_data)
|
||||
|
||||
# Assert
|
||||
self.assertEqual(id(instance), original_instance_id)
|
||||
self.assertEqual(instance["name"], "New")
|
||||
self.assertEqual(instance["age"], 20)
|
||||
with self.assertRaises(KeyError):
|
||||
self.assertEqual(instance["city"], "Moscow")
|
||||
@@ -20,7 +20,7 @@ class ModelUtils:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def concat_data(cls, data: dict[str, Any] | Model, target: dict[str, Any], ) -> None:
|
||||
def concat_data(cls, data: dict[str | int, Any] | Model, target: dict[str | int, Any], ) -> None:
|
||||
"""
|
||||
Объединяет данные
|
||||
|
||||
@@ -31,6 +31,8 @@ class ModelUtils:
|
||||
# Получаем объект данных
|
||||
if isinstance(data, Model):
|
||||
source = model_to_dict(data)
|
||||
if 'id' in source and source['id'] is None:
|
||||
del source['id']
|
||||
else:
|
||||
source = data
|
||||
|
||||
@@ -48,6 +50,21 @@ class ModelUtils:
|
||||
:param validated_data: Данные для обновления
|
||||
"""
|
||||
|
||||
# Для модели и словаря набор полей строится по-разному
|
||||
if isinstance(instance, Model):
|
||||
is_model = True
|
||||
fields = [field.name for field in instance._meta.get_fields()]
|
||||
elif isinstance(instance, dict):
|
||||
is_model = False
|
||||
fields = instance.keys()
|
||||
else:
|
||||
raise TypeError('Instance must be a Model or dict')
|
||||
|
||||
# Назначаем только существующие поля
|
||||
for attr, value in validated_data.items():
|
||||
if hasattr(instance, attr):
|
||||
if attr in fields:
|
||||
if is_model:
|
||||
setattr(instance, attr, value)
|
||||
else:
|
||||
# noinspection unresolved-references
|
||||
instance[attr] = value
|
||||
|
||||
@@ -23,6 +23,7 @@ dependencies = [
|
||||
"gunicorn",
|
||||
"pillow",
|
||||
"psycopg[binary]",
|
||||
"pytz",
|
||||
"redis",
|
||||
"split_settings",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user