Тестирование

В этом разделе описаны практики тестирования приложений на основе 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

Советы по тестированию

  1. Изолируйте тесты: Каждый тест должен быть независимым от других.

  2. Моки для внешних интеграций: БД, HTTP-клиенты и т.п. подменяйте заглушками и провайдерами dishka.

  3. Покрывайте ошибочные сценарии: Некорректные флаги, неизвестные команды, пустой ввод.

  4. Минимизируйте зависимость от форматирования: Сравнивайте ключевые фрагменты вывода, а не весь блок целиком.

  5. Измеряйте покрытие: Используйте pytest-cov.