From 767d74206003be4797129e169e33a59f2ba36a3b Mon Sep 17 00:00:00 2001 From: kolo Date: Mon, 3 Nov 2025 14:53:07 +0300 Subject: [PATCH] docs --- .../testing/app_integration_unittest.py | 26 ++++ .../testing/di_handler_unittest.py | 60 +++++++++ .../testing/simple_handler_unittest.py | 24 ++++ docs/index.rst | 10 +- docs/root/testing.rst | 123 ++++-------------- 5 files changed, 145 insertions(+), 98 deletions(-) create mode 100644 docs/code_snippets/testing/app_integration_unittest.py create mode 100644 docs/code_snippets/testing/di_handler_unittest.py create mode 100644 docs/code_snippets/testing/simple_handler_unittest.py diff --git a/docs/code_snippets/testing/app_integration_unittest.py b/docs/code_snippets/testing/app_integration_unittest.py new file mode 100644 index 0000000..c4638a9 --- /dev/null +++ b/docs/code_snippets/testing/app_integration_unittest.py @@ -0,0 +1,26 @@ +import io +import unittest +from contextlib import redirect_stdout + +from argenta import App, Router, Command, Response +from argenta.command import InputCommand + + +class TestAppIntegration(unittest.TestCase): + def setUp(self) -> None: + self.app = App(override_system_messages=True, repeat_command_groups=False) + self.router = Router(title="App") + + @self.router.command(Command("HELP", description="Show help")) + def help_cmd(response: Response): + print("Available commands: HELP") + _ = help_cmd # appease linter: function is registered via decorator + + self.app.include_router(self.router) + + def test_help_command(self): + with redirect_stdout(io.StringIO()) as stdout: + self.router.finds_appropriate_handler(InputCommand.parse("HELP")) + self.assertIn("Available commands:", stdout.getvalue()) + + diff --git a/docs/code_snippets/testing/di_handler_unittest.py b/docs/code_snippets/testing/di_handler_unittest.py new file mode 100644 index 0000000..789ee96 --- /dev/null +++ b/docs/code_snippets/testing/di_handler_unittest.py @@ -0,0 +1,60 @@ +import io +import unittest +from contextlib import redirect_stdout +from unittest.mock import Mock + +from dishka import Provider, provide, make_container, Scope # pyright: ignore[reportUnknownVariableType] + +from argenta import Router, Command +from argenta.di.integration import inject, setup_dishka, FromDishka +from argenta.response import Response +from argenta.response.status import ResponseStatus + + +class Service: + def hello(self) -> str: + return "world" + +router = Router(title="DI") + + +@router.command(Command("HELLO")) +@inject # Auto-inject dependencies from the Response container +def hello(response: Response, service: FromDishka[Service]) -> None: + print(f"hello {service.hello()}") + + +class TestDIHandler(unittest.TestCase): + def test_hello_uses_service(self): + # Prepare DI container with a stub + fake = Mock(spec=Service) + fake.hello.return_value = "mocked" + + class TestProvider(Provider): + scope = Scope.APP + + @provide(scope=Scope.APP) + def service(self) -> Service: + return fake + + container = make_container(TestProvider()) + + # Bind container to Response via integration + setup_dishka(app=_FakeApp(), container=container, auto_inject=False) # type: ignore[arg-type] + + # Create Response bound to the container + r = Response(ResponseStatus.ALL_FLAGS_VALID) + r._dishka_container = container # direct assignment is acceptable in tests # pyright: ignore[reportPrivateUsage] + + # Call handler directly + with redirect_stdout(io.StringIO()) as stdout: + hello(r) + + self.assertIn("hello mocked", stdout.getvalue()) + + +class _FakeApp: + # Minimal stub for setup_dishka; app object is not used in unit tests + registered_routers = [] + + diff --git a/docs/code_snippets/testing/simple_handler_unittest.py b/docs/code_snippets/testing/simple_handler_unittest.py new file mode 100644 index 0000000..81a5e04 --- /dev/null +++ b/docs/code_snippets/testing/simple_handler_unittest.py @@ -0,0 +1,24 @@ +import io +import unittest +from contextlib import redirect_stdout + +from argenta import Router, Command, Response +from argenta.command import InputCommand + + +router = Router(title="Demo") + + +@router.command(Command("PING", description="Ping command")) +def ping(response: Response): + print("PONG") + + +class TestSimpleHandler(unittest.TestCase): + def test_ping_prints_pong(self): + # Имитация запуска хендлера через роутер + with redirect_stdout(io.StringIO()) as stdout: + router.finds_appropriate_handler(InputCommand.parse("PING")) + self.assertIn("PONG", stdout.getvalue()) + + diff --git a/docs/index.rst b/docs/index.rst index 336f9a9..ef5a4c9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -42,11 +42,17 @@ Argenta предназначена для создания приложений, root/quickstart root/error_handling root/flags - root/dependency_injection root/overriding_formatting - root/redirect_stdout root/api/index +.. toctree:: + :hidden: + :caption: Продвинутое использование: + + root/redirect_stdout + root/dependency_injection + root/testing + .. toctree:: :hidden: :caption: Для разработчиков: diff --git a/docs/root/testing.rst b/docs/root/testing.rst index 2be52c9..cbe81c5 100644 --- a/docs/root/testing.rst +++ b/docs/root/testing.rst @@ -1,116 +1,47 @@ Тестирование ============ -В этом разделе описаны рекомендации и лучшие практики по тестированию приложений, построенных с использованием Argenta. +В этом разделе описаны практики тестирования приложений на основе ``Argenta``. Примеры основаны на фактическом публичном API: ``App``, ``Router``, ``Command``, ``Orchestrator``, DI через ``dishka`` и интеграцию в ``argenta.di.integration``. -Модульное тестирование ----------------------- +Модульное тестирование хендлеров +-------------------------------- -Для модульного тестирования команд рекомендуется использовать стандартный модуль ``unittest`` или любой другой предпочитаемый фреймворк (например, ``pytest``). +Обработчики в Argenta — обычные функции. Их удобно тестировать как чистые функции, не поднимая весь цикл приложения. Рекомендуются ``unittest`` или ``pytest``. -Пример теста для простой команды: +Пример с ``unittest`` для простого хендлера без DI: -.. code-block:: python +.. literalinclude:: ../code_snippets/testing/simple_handler_unittest.py + :language: python + :linenos: - import unittest - from unittest.mock import MagicMock - from your_app import app, your_command_handler +Тестирование с внедрением зависимостей (DI) +------------------------------------------- - class TestYourCommand(unittest.TestCase): - def test_your_command_handler(self): - # Подготовка тестовых данных - mock_scope = MagicMock() - test_args = {"arg1": "test_value"} - - # Вызов обработчика - result = your_command_handler(scope=mock_scope, **test_args) - - # Проверка результата - self.assertEqual(result, "expected_result") - mock_scope.some_dependency.assert_called_once_with("test_value") +Если хендлеру нужны зависимости, используйте ``dishka`` и интеграцию Argenta: +.. literalinclude:: ../code_snippets/testing/di_handler_unittest.py + :language: python + :linenos: -Тестирование с зависимостями ----------------------------- +Интеграционное тестирование приложения +-------------------------------------- -При использовании внедрения зависимостей через Dishka, вы можете использовать моки для тестирования: +Для более высокого уровня тестов собирайте ``App`` и ``Router`` и вызывайте хендлеры через парсинг команд, обходя бесконечный цикл ввода. Это даёт близкое к реальности поведение без необходимости симулировать ``stdin``. -.. code-block:: python +.. literalinclude:: ../code_snippets/testing/app_integration_unittest.py + :language: python + :linenos: - from dishka import make_async_container - from dishka.integrations.base import wrap_injection - from unittest.mock import AsyncMock - - class TestWithDependencies(unittest.IsolatedAsyncioTestCase): - async def test_handler_with_dependencies(self): - # Создаем мок-контейнер - mock_dependency = AsyncMock() - mock_dependency.some_method.return_value = "mocked_result" - - # Настраиваем контейнер - container = MagicMock() - container.get.return_value = mock_dependency - - # Оборачиваем обработчик для тестирования - handler = wrap_injection( - your_handler, - container=container, - ) - - # Вызываем обработчик - result = await handler(arg1="test") - - # Проверяем результаты - self.assertEqual(result, "expected_result") - mock_dependency.some_method.assert_called_once() - - -Интеграционное тестирование ---------------------------- - -Для тестирования всего приложения целиком можно использовать клиент тестирования: - -.. code-block:: python - - from argenta import Application - from io import StringIO - import unittest - - class TestAppIntegration(unittest.TestCase): - def setUp(self): - self.app = Application() - self.app.setup() # Инициализация приложения - self.output = StringIO() - self.app.stdout = self.output - - def test_help_command(self): - self.app.process_input("help") - output = self.output.getvalue() - self.assertIn("Доступные команды:", output) +E2E-тестирование цикла (опционально) +------------------------------------ +Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк во ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — вынесите конфигурацию в функцию ``main()`` и запускайте модуль в подпроцессе с подготовленным вводом/выводом. Советы по тестированию ---------------------- 1. **Изолируйте тесты**: Каждый тест должен быть независимым от других. -2. **Используйте моки**: Заменяйте внешние зависимости моками для изоляции тестируемого кода. -3. **Проверяйте граничные случаи**: Уделяйте внимание краевым случаям и ошибочным сценариям. -4. **Тестируйте обработку ошибок**: Убедитесь, что ваше приложение корректно обрабатывает ошибки. -5. **Измеряйте покрытие**: Используйте инструменты вроде ``coverage.py`` для анализа покрытия кода тестами. - -Пример настройки ``pytest`` с покрытием кода: - -.. code-block:: ini - - # setup.cfg - [tool:pytest] - testpaths = tests - python_files = test_*.py - addopts = -v --cov=your_package --cov-report=term-missing - -Для запуска тестов с покрытием: - -.. code-block:: bash - - pip install pytest-cov - pytest +2. **Моки для внешних интеграций**: БД, HTTP-клиенты и т.п. подменяйте заглушками и провайдерами ``dishka``. +3. **Покрывайте ошибочные сценарии**: Некорректные флаги, неизвестные команды, пустой ввод. +4. **Минимизируйте зависимость от форматирования**: Сравнивайте ключевые фрагменты вывода, а не весь блок целиком. +5. **Измеряйте покрытие**: Используйте ``pytest-cov``.