Привет Хабр! Меня зовут Антон и я 4 года работаю QA . За это время я успел пройти путь от книжки Савина (куда же без неё) до организации процессов тестирования на небольших проектах.

В частности, в процессе автоматизации тестирования иногда появляются интересные задачи, к которым, на первый взгляд, абсолютно непонятно как подступиться. Об одной из таких задач сегодня и пойдёт речь.

Дано

На проекте есть Telegram бот с админкой. Соответственно действия которые произведены в админке отражаются для клиента в боте.

Понятно, что можно протестировать работу админки отдельно, потом для бота мокнуть внешние зависимости и проверить его в отрыве от остального функционала. Но что же с E2E?

Определение требований

Нужен инструмент, который позволит:

  1. Тестировать бота в привязке к админке в вебе

  2. Работать с UI тестами

  3. Работать с API тестами

  4. (желательно) Не зависеть от выбора инструмента автоматизированного тестирования

Поиск готовых решений

По запросам похожим на “Автоматизация тестирования бота telegram” - несколько десятков статей про юнит-тесты, несколько видео про тестирование диалога с ботом и ничего по интересующей конкретно меня теме, возможно плохо искал, но я правда старался.

В какой-то момент даже нашёл относительно близкое к желаемому - фреймворк Botium, но, увы, у него не оказалось коннектора для Telegram. Так что я всё таки решился изобрести свой велосипед…

Идея

Первая идея, мне нужен собственный телеграмм бот, который будет слушать тестируемого бота и сам посылать к нему запросы и возвращать полученные ответы. Возникло сразу две проблемы:

  1. В процессе изучения Telegram Bot API оказалось, что один бот не может писать другому боту и вообще “слышать” его.

  2. Все библиотеки реализующие ботов как правило асинхронные, а тесты у меня вполне себе синхронны.

Всё это осложнялось тем, что я никогда не имел ничего общего с разработкой ботов. В общем, образовался небольшой тупичок, из которого предстояло как-то найти выход.

Формирование решения

Итак, план, в целом, ясен, осталось только решить образовавшиеся проблемы.

Telegram API

С первой проблемой помог Хабр, а именно эта статья.

Оказалось у Telegram, помимо Telegram Bot API, есть ещё и Telegram API, который, как я понял, подразумевался как способ создания собственного Telegram клиента. Но так же это API даёт такую интересную возможность как создание, так называемых, юзер-ботов, которые работают под обычными пользователями, что позволяет такому боту взаимодействовать с обычным ботом написанным на Telegram Bot API.

Проблема асинхронности

Тесты синхронные, а бот-тестировщик асинхронный. А такая ли это проблема? Пусть бот работает независимо от тестов, бот будет просто читать и записывать сообщения в какую-нибудь очередь и ждать команд от тестов на отправку.

В итоге мы имеем следующую схему:

  • Есть тесты, они просто записывают куда-то команды боту-тестировщику на отправку сообщения, либо читают сообщения, которые он прочитал и записал в очередь.

  • Сам бот-тестировщик работает абсолютно независимо от тестов. Просто ловит событие полученного сообщения от тестируемого бота и слушает очередь, в которую тесты записывают сообщения на отправку. Когда в очереди появляются записи, бот отправляет текст записи тестируемому боту.

Выбор инструментов

Реализовать свою затею решил на самом “ботовом” языке программирования (ну и на самом мне близком) - Python.

Есть несколько библиотек, которые реализуют взаимодействие через Telegram API, мне приглянулся Telethon.

Для реализации очереди сообщений использовал сервер Redis и одноимённую библиотеку Python.

Написание кода

Настройка сессии

Опустим описание процесса регистрации приложения на my.telegram.org и приступим к реализации самого бота. (подробнее о регистрации можно узнать тут)

from telethon import TelegramClient, sync

# Название сессии
session = 'tester_bot'
# Api ID и Api Hash полученные на my.telegram.org
api_id = 12345678
api_hash = '123456789qwerty987654321'

client = TelegramClient(session, api_id, api_hash)

async def main():
    # Выводим в консоль данные о текущем пользователе, для проверки
    me = await client.get_me()
    print(me.stringify())
    
    # Сюда в дальнейшем добавим вызов метода отправки сообщений
    
    # Бот будет запущен пока мы сами не завершим его работу
    await client.run_until_disconnected()

if __name__ == '__main__':
    with client:
        client.loop.run_until_complete(main())

После запуска скрипта в консоли надо ввести телефон от аккаунта Telegram, а затем код подтверждения. Потом в папке со скриптом должен создаться файл .session, который сохраняет вашу сессию и позволяет запускать бота без повторной авторизации через телефон + код.

Важно! Пока разбирался с библиотекой, Telegram счёл мои действия подозрительными, поэтому просто обрывал все сессии с моего IP. В дальнейшем я смог создать стабильную сессию, только с использованием VPN.

Очень рекомендую при получении кода в telegram заходить в официальный клиент с другого IP и устройства (например с телефона через мобильный интернет). И вообще стараться не дёргаться лишний раз до создания сессии.

Что ж, сессия настроена, но бот пока не может ни читать, ни отправлять сообщения

Redis

Для чтения и отправки сообщений из тестов потребуется Redis. Запускаем redis-server и добавляем вызов соединения с ним в скрипт.

from redis import Redis

redis = Redis(host=localhost, port=6379)

Чтение сообщений

Читаем сообщения и записываем их в очередь для полученных сообщений в Redis.

@client.on(events.NewMessage())
async def handle_new_message(event):
    try:
        if event.is_private:
            sender_username = await event.get_sender().username
            if sender_username == "username тестируемого бота":
                # Записываем полученное сообщение от тестируемого бота в очередь
                redis.rpush('response_messages', message.message)
    except Exception as e:
        print(f'Ошибка при чтении сообщения: {e}')

Отправка сообщений

А теперь читаем сообщения из очереди на отправку и отправляем их в Telegram.

async def handle_messages_to_send():
    while True:
        try:
            # Проверяем, что очередь не пустая
            if redis.llen('request_messages') != 0:
                # Забираем сообщение из очереди на отправку, декодируем и отправляем
                message = redis.rpop('request_messages').decode()
                await client.send_message("username тестируемого бота", message)
        except Exception as e:
            print(e)
            await asyncio.sleep(5)

Не забываем добавить вызов этой функции в main.

На этом базовая версия бота готова! Просто запускаем этот скрипт и забываем. Можно переходить к разработке самих тестов.

Тесты

На самих тестах зацикливаться не буду. Работаем с redis как будто это и есть бот. Покажу на примере pytest.

@pytest.fixture()
def tester_bot():
  redis = Redis(host='localhost', port=6379)
  yield redis
  # Чистим очереди после выполнения тестов
  redis.delete('response_messages')
  redis.delete('request_messages')

def test_response_message(tester_bot):
  # Выполяем какие-то действия через браузер или API
  ...
  # Получаем сообщение от бота
  message = tester_bot.rpop('response_messages').decode()
  assert message == 'Ожидаемое сообщение'

def test_request_message(tester_bot)
  # Закидываем сообщение в очередь на отправку
  tester_bot.rpush('request_messages', 'Привет')
  # Добавляем какое-нибудь ожидание
  time.sleep(2)
  # Получаем ответ от бота
  message = tester_bot.rpop('response_messages').decode()
  assert message == 'Привет, я бот! Получил твоё сообщение!'

По поводу ожиданий. Чтобы не ждать "глупо", можно реализовать ожидание, пока длина очереди полученных сообщений не изменится, хоть на том же WebDriverWait из Selenium.

Итог

Готово! Бот работает и отлично подключается к тестам, так же абсолютно не важно на каком языке вы пишите тесты, нужно только реализовать в тестах подключение к Redis. Конечно, для полноценной интеграции таких тестов в проект, необходимо дополнительно обернуть реализацию в удобный интерфейс, но целью статьи было показать саму концепцию таким же QA в поисках решения как и я!

Полный код на GitHub

Уверен, что всё что-то реализовано не очень хорошо. Если так, то буду только рад обсудить это в комментариях!