mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 10:05:28 +03:00
docs
This commit is contained in:
@@ -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())
|
||||||
|
|
||||||
|
|
||||||
@@ -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 = []
|
||||||
|
|
||||||
|
|
||||||
@@ -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())
|
||||||
|
|
||||||
|
|
||||||
+8
-2
@@ -42,11 +42,17 @@ Argenta предназначена для создания приложений,
|
|||||||
root/quickstart
|
root/quickstart
|
||||||
root/error_handling
|
root/error_handling
|
||||||
root/flags
|
root/flags
|
||||||
root/dependency_injection
|
|
||||||
root/overriding_formatting
|
root/overriding_formatting
|
||||||
root/redirect_stdout
|
|
||||||
root/api/index
|
root/api/index
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:hidden:
|
||||||
|
:caption: Продвинутое использование:
|
||||||
|
|
||||||
|
root/redirect_stdout
|
||||||
|
root/dependency_injection
|
||||||
|
root/testing
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:hidden:
|
:hidden:
|
||||||
:caption: Для разработчиков:
|
:caption: Для разработчиков:
|
||||||
|
|||||||
+27
-96
@@ -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
|
Тестирование с внедрением зависимостей (DI)
|
||||||
from unittest.mock import MagicMock
|
-------------------------------------------
|
||||||
from your_app import app, your_command_handler
|
|
||||||
|
|
||||||
class TestYourCommand(unittest.TestCase):
|
Если хендлеру нужны зависимости, используйте ``dishka`` и интеграцию Argenta:
|
||||||
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")
|
|
||||||
|
|
||||||
|
.. 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
|
E2E-тестирование цикла (опционально)
|
||||||
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)
|
|
||||||
|
|
||||||
|
Полный запуск цикла ``start_polling`` можно покрывать через подпроцесс с передачей строк во ``stdin``. Это тяжелее и обычно не требуется. Если всё же необходимо — вынесите конфигурацию в функцию ``main()`` и запускайте модуль в подпроцессе с подготовленным вводом/выводом.
|
||||||
|
|
||||||
Советы по тестированию
|
Советы по тестированию
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
1. **Изолируйте тесты**: Каждый тест должен быть независимым от других.
|
1. **Изолируйте тесты**: Каждый тест должен быть независимым от других.
|
||||||
2. **Используйте моки**: Заменяйте внешние зависимости моками для изоляции тестируемого кода.
|
2. **Моки для внешних интеграций**: БД, HTTP-клиенты и т.п. подменяйте заглушками и провайдерами ``dishka``.
|
||||||
3. **Проверяйте граничные случаи**: Уделяйте внимание краевым случаям и ошибочным сценариям.
|
3. **Покрывайте ошибочные сценарии**: Некорректные флаги, неизвестные команды, пустой ввод.
|
||||||
4. **Тестируйте обработку ошибок**: Убедитесь, что ваше приложение корректно обрабатывает ошибки.
|
4. **Минимизируйте зависимость от форматирования**: Сравнивайте ключевые фрагменты вывода, а не весь блок целиком.
|
||||||
5. **Измеряйте покрытие**: Используйте инструменты вроде ``coverage.py`` для анализа покрытия кода тестами.
|
5. **Измеряйте покрытие**: Используйте ``pytest-cov``.
|
||||||
|
|
||||||
Пример настройки ``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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user