Когда unit-тесты лгут: боевые испытания Telegram-бота

Telegram-бот на боевых испытаниях: когда unit-тесты не подстраховывают
Проект bot-social-publisher начинался просто. Полнофункциональный Telegram-бот с памятью, командами, интеграциями. Но вот на очередную спринт-планерку я заявил: добавим систему управления доступом. Идея казалась пустяковой — дать владельцам возможность приватизировать свои чаты, чтобы только они могли с ботом общаться. Типичный use case: персональный AI-ассистент или модератор в закрытой группе.
Теория была прекрасна. Я развернул ChatManager — специальный класс с методом is_allowed(), который проверяет, разрешена ли пользователю отправка сообщений в конкретный чат. Добавил миграцию SQLite для таблицы managed_chats, прошил middleware в aiogram, написал обработчики команд /manage add, /manage remove, /manage status, /manage list. Unit-тесты прошли с зелёным светом — pytest даже не чихнул. Документация пока отложена, но это же детали!
Потом наступил момент истины.
Запустил бота локально через python telegram_main.py, переключился в личный чат и отправил первую /manage add. Бот записал ID чата, переключился в режим приватности. Нормально! Попробовал отправить обычное сообщение — ответ пришёл. Открыл чат со своего второго аккаунта, отправил то же самое — тишина. Бот ничего не ответил. Перфект, middleware работает.
Но не всё было так гладко.
Первая проблема вылезла при быстрых командах подряд. В асинхронной архитектуре aiogram и aiosqlite есть коварная особенность: middleware может проверить разрешения раньше, чем транзакция успела закоммититься. Получилась гонка условий — бот получал /manage add, начинал записывать в БД, но его собственная система контроля доступа успевала выполнить проверку за доли секунды до того, как данные попали в таблицу. Казалось бы, логические ошибки не могут быть незаметны в коде, но тут они проявились только в полевых условиях.
Вторая проблема — SQLite при одновременной работе нескольких асинхронных обработчиков. Один handler записывал изменение в БД, а другой в это время проверял состояние — и видел старые данные, потому что commit() ещё не произошёл. Гарантировать консистентность мне помогли явные транзакции и аккуратная работа с await’ами.
Вот в чём прелесть интеграционного тестирования: ты отправляешь реальное сообщение через Telegram-серверы, оно проходит через webhook, пробегает весь стек middleware, обрабатывается обработчиком, записывается в БД и возвращается пользователю. Unit-тесты проверяют логику функции. Интеграционные тесты проверяют, работает ли всё это вместе в реальности. И оказалось, что между «работает в тесте» и «работает в реальности» огромная разница.
После всех боевых испытаний я заполнил чеклист: проверка импортов класса, валидация миграции, тестирование всех команд в Telegram, запуск полного набора pytest, документирование в docs/CHAT_MANAGEMENT.md с примерами и описанием архитектуры. Восемь пунктов — восемь потенциальных точек отказа, которые благополучно миновали.
Урок на будущее: когда работаешь с асинхронностью и базами данных, unit-тесты — это необходимо, но недостаточно. Реальный Telegram, реальные пользователи, реальная асинхронность покажут то, что никогда не отловить в тестовом окружении.
😄 Иногда мне кажется, что в облаке GCP ошибка при доступе просто уходит в облака, так что никто её не найдёт.
Метаданные
- Session ID:
- grouped_C--projects-bot-social-publisher_20260209_1219
- Branch:
- main
- Dev Joke
- GCP: решение проблемы, о существовании которой ты не знал, способом, который не понимаешь.
Часть потока:
Разработка: bot-social-publisher