diff --git a/net/xeaf/rack/core/core_response.py b/net/xeaf/rack/core/core_response.py index 35e5220..a8532eb 100644 --- a/net/xeaf/rack/core/core_response.py +++ b/net/xeaf/rack/core/core_response.py @@ -41,7 +41,7 @@ class CoreResponse(Response): result["detail"] = data # Набор дополнительных данных - if meta: + if meta is not None: result["meta"] = meta # Формируем ответ diff --git a/net/xeaf/rack/tests/core/__init__.py b/net/xeaf/rack/tests/core/__init__.py index 809a448..8645f2d 100644 --- a/net/xeaf/rack/tests/core/__init__.py +++ b/net/xeaf/rack/tests/core/__init__.py @@ -13,3 +13,4 @@ from .core_enum_choices_tests import CoreEnumChoicesTests from .core_enum_from_string_tests import CoreEnumFromStringTests from .core_enum_keys_tests import CoreEnumKeysTests from .core_enum_values_tests import CoreEnumValuesTests +from .core_response_tests import CoreResponseTests diff --git a/net/xeaf/rack/tests/core/core_response_tests.py b/net/xeaf/rack/tests/core/core_response_tests.py new file mode 100644 index 0000000..26150a0 --- /dev/null +++ b/net/xeaf/rack/tests/core/core_response_tests.py @@ -0,0 +1,307 @@ +# DRF Rack +# Библиотека классов расширений для Django REST Framework +# +# Автор: Николай В. Анохин +# Все права защищены. Лицензия: MIT + +""" +Описание класса CoreResponseTests +""" + +from rest_framework import status +from rest_framework.response import Response + +from net.xeaf.rack.core import CoreResponse +from net.xeaf.rack.core.testing import CoreSimpleTestCase + + +class CoreResponseTests(CoreSimpleTestCase): + """ + Тестовый класс для метода CoreResponse.__init__() + """ + + def test_init_with_success_status_and_data(self): + """ + Тест проверяет инициализацию ответа с успешным статусом и данными + """ + + # Arrange + internal_status = status.HTTP_200_OK + external_status = status.HTTP_200_OK + data = {"key": "value", "list": [1, 2, 3]} + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertIn("timestamp", response.data) + self.assertEqual(response.data["data"], data) + self.assertNotIn("detail", response.data) + self.assertNotIn("meta", response.data) + + def test_init_with_success_status_and_string_data(self): + """ + Тест проверяет инициализацию ответа с успешным статусом и строковыми данными + """ + + # Arrange + internal_status = status.HTTP_201_CREATED + external_status = status.HTTP_201_CREATED + data = "Operation completed successfully" + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["data"], data) + self.assertNotIn("detail", response.data) + + def test_init_with_success_status_and_bool_data(self): + """ + Тест проверяет инициализацию ответа с успешным статусом и булевыми данными + """ + + # Arrange + internal_status = status.HTTP_200_OK + external_status = status.HTTP_200_OK + data = True + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["data"], data) + self.assertNotIn("detail", response.data) + + def test_init_with_success_status_and_none_data(self): + """ + Тест проверяет инициализацию ответа с успешным статусом и None в качестве данных + """ + + # Arrange + internal_status = status.HTTP_204_NO_CONTENT + external_status = status.HTTP_204_NO_CONTENT + data = None + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertNotIn("data", response.data) + self.assertNotIn("detail", response.data) + + def test_init_with_error_status_and_detail(self): + """ + Тест проверяет инициализацию ответа с ошибочным статусом и деталями ошибки + """ + + # Arrange + internal_status = status.HTTP_400_BAD_REQUEST + external_status = status.HTTP_400_BAD_REQUEST + data = "Invalid request parameters" + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["detail"], data) + self.assertNotIn("data", response.data) + + def test_init_with_error_status_and_dict_detail(self): + """ + Тест проверяет инициализацию ответа с ошибочным статусом и деталями в виде словаря + """ + + # Arrange + internal_status = status.HTTP_422_UNPROCESSABLE_ENTITY + external_status = status.HTTP_422_UNPROCESSABLE_ENTITY + data = {"field": "email", "error": "Invalid email format"} + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["detail"], data) + self.assertNotIn("data", response.data) + + def test_init_with_server_error_status_and_detail(self): + """ + Тест проверяет инициализацию ответа с серверной ошибкой и деталями + """ + + # Arrange + internal_status = status.HTTP_500_INTERNAL_SERVER_ERROR + external_status = status.HTTP_500_INTERNAL_SERVER_ERROR + data = "Internal server error occurred" + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["detail"], data) + self.assertNotIn("data", response.data) + + def test_init_with_meta_data(self): + """ + Тест проверяет инициализацию ответа с дополнительными мета-данными + """ + + # Arrange + internal_status = status.HTTP_200_OK + external_status = status.HTTP_200_OK + data = {"id": 123} + meta = {"page": 1, "total": 100, "per_page": 10} + + # Act + response = CoreResponse(internal_status, external_status, data, meta) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["data"], data) + self.assertEqual(response.data["meta"], meta) + + def test_init_with_error_status_and_meta(self): + """ + Тест проверяет инициализацию ответа с ошибочным статусом и мета-данными + """ + + # Arrange + internal_status = status.HTTP_404_NOT_FOUND + external_status = status.HTTP_404_NOT_FOUND + data = "Resource not found" + meta = {"resource_type": "user", "requested_id": 999} + + # Act + response = CoreResponse(internal_status, external_status, data, meta) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["detail"], data) + self.assertEqual(response.data["meta"], meta) + self.assertNotIn("data", response.data) + + def test_init_with_empty_meta(self): + """ + Тест проверяет инициализацию ответа с пустыми мета-данными + """ + + # Arrange + internal_status = status.HTTP_200_OK + external_status = status.HTTP_200_OK + data = {"id": 123} + meta = {} + + # Act + response = CoreResponse(internal_status, external_status, data, meta) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["data"], data) + self.assertEqual(response.data["meta"], {}) + self.assertNotIn("detail", response.data) + + def test_init_timestamp_is_integer(self): + """ + Тест проверяет, что timestamp является целым числом + """ + + # Arrange + internal_status = status.HTTP_200_OK + external_status = status.HTTP_200_OK + data = "test" + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertIn("timestamp", response.data) + self.assertIsInstance(response.data["timestamp"], int) + self.assertGreater(response.data["timestamp"], 0) + + def test_init_with_different_external_status(self): + """ + Тест проверяет инициализацию ответа с отличающимися внутренним и внешним статусами + """ + + # Arrange + internal_status = status.HTTP_200_OK + external_status = status.HTTP_201_CREATED + data = {"created": True} + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["data"], data) + self.assertNotEqual(response.status_code, internal_status) + + def test_init_with_boundary_status_values(self): + """ + Тест проверяет инициализацию ответа с граничными значениями статусов + """ + + # Arrange - тестируем границу между успехом и ошибкой (400) + internal_status = 399 + external_status = 399 + data = "Redirect" + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert - на уровне 399 это все еще считается успешным (менее 400) + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["data"], data) + self.assertNotIn("detail", response.data) + + # Arrange - тестируем границу ошибок (400) + internal_status = status.HTTP_400_BAD_REQUEST + external_status = status.HTTP_400_BAD_REQUEST + data = "Bad Request" + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert - на уровне 400 это считается ошибкой + self.assertEqual(response.status_code, external_status) + self.assertEqual(response.data["status"], internal_status) + self.assertEqual(response.data["detail"], data) + self.assertNotIn("data", response.data) + + def test_init_inherits_from_drf_response(self): + """ + Тест проверяет, что CoreResponse наследуется от DRF Response + """ + + # Arrange + internal_status = status.HTTP_200_OK + external_status = status.HTTP_200_OK + data = "test" + + # Act + response = CoreResponse(internal_status, external_status, data) + + # Assert + self.assertIsInstance(response, Response) + self.assertTrue(hasattr(response, "data")) + self.assertTrue(hasattr(response, "status_code")) + self.assertTrue(hasattr(response, "render")) diff --git a/testing_2_llm.md b/testing_2_llm.md index d31a5b4..a20cf88 100644 --- a/testing_2_llm.md +++ b/testing_2_llm.md @@ -22,13 +22,14 @@ Правила для файла тестов: 1. Изоляция: Один метод = Один тестовый класс = Один отдельный файл. 2. Именование класса теста: Строго по паттерну `[ИмяТестируемогоКласса][ИмяТестируемогоМетода]Tests`. + - СТРОГОЕ ИСКЛЮЧЕНИЕ: Если тестируется конструктор `__init__()`, то имя метода НЕ должно включаться в название класса. Класс должен называться просто `[ИмяТестируемогоКласса]Tests`. Пример: Для `DemoClass.__init__()` тестовый класс называется `DemoClassTests`. 3. Зеркалирование структуры пакетов: Тесты находятся в пакете `net.xeaf.rack.tests`. Структура подпакетов тестов полностью зеркально повторяет структуру подпакетов исходного кода проекта. 4. Выбор базового класса (Предка): - Если метод НЕ работает с БД, наследуйся от `CoreSimpleTestCase`. - Если метод работает с БД, наследуйся от `CoreUserStoryTestCase`. - Оба класса импортируются из пакета `net.xeaf.rack.core.testing`. 5. Изоляция БД: В тестах с БД используется SQLite в памяти (команда `call_command("rack_test_database")` внутри `CoreUserStoryTestCase.setUp()` автоматически сбрасывает и наполняет БД). База изолирована, данные можно изменять и удалять безболезненно. -6. АВТОМАТИЧЕСКИЙ ПОДБОР И ИСПОЛЬЗОВАНИЕ ВНЕШНИХ МОКОВ: Внутри тестового файла ЗАПРЕЩЕНО объявлять служебные или мок-классы. Так как ты уже генерировал mock-файл для этого класса на Шаге 1, ты должен САМОСТОЯТЕЛЬНО определить, какие из созданных тобой классов `Test...` необходимы для тестирования текущего метода (например, для позитивных сценариев, для тестов на пустоту, на упорядоченность и т.д.). Сформируй имя файла моков как `._[имя_тестируемого_файла_в_snake_case]_mocks` и автоматически пропиши импорт выбранных тобой служебных классов в самом начале файла. +6. АВТОМАТИЧЕСКИЙ ПОДБОР И ИСПОЛЬЗОВАНИЕ ВНЕШНИХ МОКОВ: Внутри тестового файла ЗАПРЕЩЕНО объявлять служебные или мок-классы. Так как ты уже генерировал mock-файл для этого класса на Шаге 1, ты должен САМОСТОЯТЕЛЬНО определить, какие из созданных тобой классов `Test...` необходимы для тестирования текущего метода. Сформируй имя файла моков как `._[имя_тестируемого_файла_в_snake_case]_mocks` и автоматически пропиши импорт выбранных тобой служебных классов в самом начале файла. ### ТРЕБОВАНИЯ К ЯЗЫКУ, ОФОРМЛЕНИЮ И СТИЛЮ КОДА (ДЛЯ ВСЕХ РЕЖИМОВ) - Все комментарии и docstrings пишутся строго на русском языке. @@ -39,12 +40,13 @@ Тестовый класс для DemoClass.demo_method() """ ``` + Для конструктора паттерн аналогичен: `Тестовый класс для DemoClass.__init__()`. - Оформление тестовых методов: Докстринг внутри каждого тестового метода должен быть многострочным, оформленным с пустыми строками до и после текста. -- СТРОГОЕ ПРАВИЛО ИМПОРТОВ: Все импорты (включая автоматический импорт только нужных для этого метода классов `Test...` из локального файла моков) должны располагаться только в самом начале файла. Импорты внутри классов или методов не допускаются. +- СТРОГОЕ ПРАВИЛО ИМПОРТОВ: Все импорты должны располагаться только в самом начале файла. Импорты внутри классов или методов не допускаются. - Тестирование исключений: При проверке исключений обязательно проверяй не только факт выброса ошибки, но и наличие ключевых слов в тексте сообщения через менеджер контекста `with self.assertRaises(ValueError) as context:`. - Формат ответа: Возвращай ТОЛЬКО чистый код на Python внутри одного блока разметки ```python. Без вводных фраз и пояснений после кода. ### АЛГОРИТМ ТВОЕЙ РАБОТЫ Пользователь пришлет тебе команду. - Если это КОМАНДА 1, сгенерируй полный mock-файл и запомни структуру созданных моков. -- Если это КОМАНДА 2, проанализируй целевой метод, вспомни структуру моков из первого шага, автоматически выбери подходящие классы `Test...`, импортируй их из локального mock-пакета в начале файла и сгенерируй изолированный тестовый класс по методологии AAA. +- Если это КОМАНДА 2, проанализируй целевой метод (с учетом исключения для `__init__`), вспомни структуру моков из первого шага, автоматически выбери подходящие классы `Test...`, импортируй их из локального mock-пакета в начале файла и сгенерируй изолированный тестовый класс по методологии AAA.