Добавлены тесты для Environment

This commit is contained in:
2026-07-23 16:49:31 +03:00
parent 7f25095014
commit abd999d245
8 changed files with 945 additions and 3 deletions

View File

@@ -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 * from .settings import *

View 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)

View 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)

View 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)

View 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")

View 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")

View File

@@ -20,7 +20,7 @@ class ModelUtils:
""" """
@classmethod @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): if isinstance(data, Model):
source = model_to_dict(data) source = model_to_dict(data)
if 'id' in source and source['id'] is None:
del source['id']
else: else:
source = data source = data
@@ -48,6 +50,21 @@ class ModelUtils:
:param validated_data: Данные для обновления :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(): for attr, value in validated_data.items():
if hasattr(instance, attr): if attr in fields:
setattr(instance, attr, value) if is_model:
setattr(instance, attr, value)
else:
# noinspection unresolved-references
instance[attr] = value

View File

@@ -23,6 +23,7 @@ dependencies = [
"gunicorn", "gunicorn",
"pillow", "pillow",
"psycopg[binary]", "psycopg[binary]",
"pytz",
"redis", "redis",
"split_settings", "split_settings",
] ]