Тестирование¶
В этом разделе описаны практики тестирования приложений на основе Argenta. Примеры основаны на фактическом публичном API.
Модульное тестирование обработчиков¶
Обработчики в Argenta — обычные функции. Их удобно тестировать как чистые функции, не поднимая весь цикл приложения. Рекомендуются unittest или pytest.
Пример использования:
1import io
2from contextlib import redirect_stdout
3
4from argenta import Router, Command, Response
5from argenta.command import InputCommand
6
7
8router = Router(title="Demo")
9
10
11@router.command(Command("PING", description="Ping command"))
12def ping(response: Response):
13 print("PONG")
14
15
16def test_ping_prints_pong():
17 # Call handler
18 with redirect_stdout(io.StringIO()) as stdout:
19 router.finds_appropriate_handler(InputCommand.parse("PING"))
20 assert "PONG" in stdout.getvalue()
Тестирование с внедрением зависимостей (DI)¶
Если обработчику нужны зависимости, используйте dishka и интеграцию Argenta:
Пример использования:
1import io
2from contextlib import redirect_stdout
3
4from argenta.command import InputCommand
5from dishka import Provider, make_container, Scope
6
7from argenta import Router, Response
8from argenta.di.integration import setup_dishka, FromDishka
9
10
11class Service:
12 def hello(self) -> str:
13 return "world"
14
15
16def get_service() -> Service:
17 return Service()
18
19
20router = Router(title="DI")
21
22
23@router.command("HELLO")
24def hello(response: Response, service: FromDishka[Service]) -> None:
25 print(f"hello {service.hello()}")
26
27
28class _FakeApp:
29 # Minimal stub for setup_dishka; app object is not used in unit tests
30 registered_routers = [router]
31
32
33def test_hello_uses_service():
34 provider = Provider(scope=Scope.APP)
35 provider.provide(get_service)
36
37 container = make_container(provider)
38 setup_dishka(app=_FakeApp(), container=container, auto_inject=True)
39
40 # Call handler
41 with redirect_stdout(io.StringIO()) as stdout:
42 router.finds_appropriate_handler(InputCommand.parse("HELLO"))
43
44 assert "hello world" in stdout.getvalue()
Интеграционное тестирование приложения¶
Для более высокого уровня тестов собирайте App и Router и вызывайте обработчики через парсинг команд, обходя бесконечный цикл ввода. Это даёт близкое к реальности поведение без необходимости симулировать stdin.
Пример использования:
1import io
2from contextlib import redirect_stdout
3
4from argenta import App, Router, Command, Response
5from argenta.command import InputCommand
6
7
8def test_simple_app() -> None:
9 app = App(override_system_messages=True, repeat_command_groups_printing=False)
10 router = Router(title="App")
11
12 @router.command(Command("HELP", description="Show help"))
13 def help_cmd(response: Response):
14 print("Available commands: HELP")
15
16 app.include_router(router)
17
18 with redirect_stdout(io.StringIO()) as stdout:
19 router.finds_appropriate_handler(InputCommand.parse("HELP"))
20
21 assert "Available commands:" in stdout.getvalue()
E2E-тестирование цикла¶
Полный запуск цикла start_polling можно покрывать через подпроцесс с передачей строк в stdin. Это тяжелее и обычно не требуется. Если всё же необходимо — пример ниже.
Опасно
Важно: Обязательно передавайте строковый триггер команды выхода последним элементом в списке side_effects при патче input.
Иначе тестируемое приложение будет ожидать ввода следующей команды и не сможет корректно завершиться.
Пример использования:
1import sys
2from unittest.mock import patch
3import pytest
4from pytest import CaptureFixture
5
6from argenta import App, Orchestrator, Router, Command, Response
7
8
9@pytest.fixture(autouse=True)
10def patched_argv():
11 with patch.object(sys, "argv", ["program.py"]):
12 yield
13
14
15def test_input_incorrect_command(capsys: CaptureFixture[str]):
16 router = Router()
17 orchestrator = Orchestrator()
18
19 @router.command(Command("test"))
20 def test(response: Response) -> None:
21 print("test command")
22
23 app = App(override_system_messages=True, printer=print)
24 app.include_router(router)
25 app.set_unknown_command_handler(lambda command: print(f"Unknown command: {command.trigger}"))
26
27 with patch("builtins.input", side_effect=["help", "q"]):
28 orchestrator.start_polling(app)
29
30 output = capsys.readouterr().out
31 assert "\nUnknown command: help\n" in output
Советы по тестированию¶
Изолируйте тесты: Каждый тест должен быть независимым от других.
Моки для внешних интеграций: БД, HTTP-клиенты и т.п. подменяйте заглушками и провайдерами
dishka.Покрывайте ошибочные сценарии: Некорректные флаги, неизвестные команды, пустой ввод.
Минимизируйте зависимость от форматирования: Сравнивайте ключевые фрагменты вывода, а не весь блок целиком.
Измеряйте покрытие: Используйте
pytest-cov.