Тестирование
Фреймворк: pytest + pytest-asyncio (asyncio_mode = auto) + respx
(моки HTTP). Дев-зависимости в requirements-dev.txt.
Запуск
Тесты смонтированы в контейнер как volume (./tests:/app/tests:ro),
поэтому правки на хосте видны сразу — docker cp не нужен.
# все тесты (моки, без сети)
docker exec rb-backend pytest
# или через compose (одноразовый run)
docker compose -f compose.rb.yml run --rm rb-backend pytest
# конкретный файл
docker exec rb-backend pytest tests/test_moysklad_webhooks.py
# покрытие
docker exec rb-backend pip install pytest-cov
docker exec rb-backend pytest --cov=src --cov-report=term-missing
Маркеры
| Маркер | Назначение |
|---|---|
| (по умолчанию) | unit/router тесты, всё замокано |
-m integration |
реальные МойСклад / Google Sheets (нужны креды) |
docker exec rb-backend pytest -m integration
Интеграционные тесты
Только read-only проверки (например, Stocks.stocks_dict).
Не выполняют запись в таблицы. Требуют валидный OMF_MS_TOKEN.
Структура тестов
| Файл | Покрытие |
|---|---|
conftest.py |
fixtures: client (TestClient), mock_sheets_all (dict по модулям), mock_moysklad_stock, mock_email, client_live |
test_base.py |
chain_get, get_uuid, get_doc_type, format_sum + реальные meta МойСклад |
test_moysklad_webhooks.py |
роутинг internal-webhook по примерам вебхуков (CREATE/UPDATE/DELETE, mixed, unknown, пустые/битые тела) |
test_routers.py |
все 5 rb-эндпоинтов через TestClient |
test_status.py |
логика статусов (update_state_by_stock, update_state_in_regular, routing) |
test_bot.py |
prepare_tg_msg (экранирование MarkdownV2) |
test_mail.py |
prepare_email_msg / prepare_email_link |
test_const.py |
константы (Kuda, excluded_product_paths, колонки) |
test_stocks.py |
Stocks, is_stock_enough |
test_celery_stub.py |
заглушка @app.task / .delay |
test_integration.py |
реальные вызовы (маркер integration) |
Принципы мокирования
Внешние вызовы (МойСклад, Google Sheets, Telegram, SMTP) патчатся в точке
использования (point-of-use), потому что модули делают
from x import Y — локальные алиасы, и патч модуля-источника их не перехватит.
# правильно — патчим локальную ссылку в модуле, где вызывается
with patch("src.endpoints.replace_buyer.handle_create_purchaseorder") as h:
client.post("/vendor_endpoint/rb/internal-webhook", json=body)
h.assert_called_once()
# Google Sheets: mock_sheets_all["status"] / ["endpoints"] / ["regular"] / ["main"]
status_sheets, status_sheet = mock_sheets_all["status"]
Текущий статус
110 passed, 0 failed.
Известные дыры покрытия (15–28%): regular.py, regular_buy.py,
table_from_doc.py, approve.py — тяжёлые сетевые обработчики. Покрыты
только точкой входа (мокаются в router-тестах). Для полного покрытия нужны
VCR-записи ответов МойСклад.