Каждый, кто когда-либо писал telegram-ботов, задавался вопросом: «А как их тестировать?» Сложно найти однозначный ответ. Например, при написании тестов для веб-приложений и API можно воспользоваться тестовым клиентом DRF или FastAPI: просто пишешь запрос и делаешь assert на полученный ответ. Мне захотелось получить подобный функционал и для тестирования telegram-бота.
Привет, Хабр. Я Михаил Выборный, python-разработчик, backend-developer в облачном провайдере beeline cloud. В этой статье я хочу поделиться опытом написания автоматизированных end-to-end-тестов без эмуляции Telegram Bot API, но с использованием тестовых аккаунтов. Мы зайдем в изолированное тестовое пространство Telegram, создадим тестового бота, подготовим фикстуру для запуска нашего приложения и напишем авторизацию для тестовых клиентов.
По ходу статьи я буду использовать инструменты следующих библиотек:
Статья написана на примере реализации несложного бота для хранения фотографий и отправки их в ответ при запросе. Вы можете посмотреть исходный код приложения и тестов на GitHub. Ниже я использую короткие выдержки из официальной документации Telegram на английском – к ним даю пояснения на русском своими словами.
Перед тем как что-то писать самому, я пытался найти готовое решение и был уверен, что Google мне поможет. Но оказалось, что готового оптимального решения просто не существует. Здесь я кратко перечислю то, что мне удалось найти и почему это не подошло.
PTB Test
Aiogram Tests
Все тест-кейсы описываются как вызов конкретных handler-ов, из-за чего нет возможности протестировать их поведения целиком — от отправки пользователем сообщения до получения ответа. К примеру, непонятно, как тестировать фильтрацию входящих сообщений и их распределение по различным handler-ам и т. п.
Tg Integration
Предположим, наш бот уже готов и мы переходим к тестированию. Для этого мы подготовим необходимые инструменты, чтобы написание самих тестов было кратким и понятным. Отправить сообщение, получить ответ, проверить его. Да, придется потратить время, чтобы реализовать вспомогательные фикстуры по регистрации клиентов и обработки входящих сообщений. Но, заморочившись однажды, мы сможем писать сами тесты всего в несколько строк. Их будет легко обновлять и поддерживать.
Отправлять тестовые сообщения мы будем с помощью Pyrogram. Для этого нужно зарегистрировать будущее клиентское приложение.
Тут важно понимать, что для взаимодействия с Telegram API необходимо зарегистрировать свое приложение в системе Telegram — для этого потребуется действующий номер телефона. Но для запуска самих тестов будут использоваться тестовые телефонные номера. Поэтому переживать за сохранность личных данных или получение флуд-бана не стоит.
После регистрации мы получаем api_key и api_id, которые можем использовать для создания множества клиентских приложений.
The API key defines a token for a Telegram application you are going to build. This means that you are able to authorize multiple users or bots with a single API key. (© Telegram Documentation)
Регистрация бота
Для тестирования пользовательских приложений Telegram дает доступ к Dedicated test environment (выделенная тестовая среда). Это изолированная песочница, где можно регистрировать пользователей и создавать ботов. Если аккаунты мы будем создавать динамически, используя тестовые номера, то зарегистрировать тестового бота у @BotFather проще вручную, используя свой настоящий номер.
The test environment is completely separate from the main environment, so you will need to create a new user account and a new bot with @BotFather. (© Telegram Documentation)
Как это сделать, можно прочитать тут. Я использовал приложение Telegram для iOS, кликнув 10 раз на иконку Settings (Настройки).
После этого в приложении становится доступен второй аккаунт. Это очень удобное свойство Telegram, о котором знают немногие (лично я раньше не знал). Можно использовать для различного тестирования или даже завести секретный чат с другим человеком (который тоже зарегистрировался в тестовом пространстве). Далее, как обычно, регистрируем бота у @BotFather и получаем токен. Для обращения к любому из методов нужно добавить /test/ в путь запроса.
Теперь при запуске тестов мы можем запустить наше приложение в тестовом пространстве. Для этого подготовим фикстуру.
@pytest.fixture(scope=’session’) # fixture runs Bot app only once for entire tess session async def application(): app = Application.builder().token(‘<token>’ + ‘/test’).build() await app.initialize() await app.post_init(app) # initialize does *not* call `post_init` — that is only done by run_polling/webhook await app.start() await app.updater.start_polling() yield app await app.updater.stop() await app.stop() await app.shutdown()
Обычно для запуска приложения мы используем app.run_polling(), но это заблокирует дальнейшее исполнение программы. Мы же будем запускать тесты в параллель с тем, как работает наш бот. Подробнее о том, как запускать PTB с другим асинхронным кодом, — Running PTB alongside other asyncio frameworks
pytest сам по себе не асинхронный, но может работать с асинхронными тестами и фикстурами, если добавить pytestmark = pytest.mark.anyio. Подробнее: Testing with AnyIO
PTB на данный момент не поддерживает работу бота в тестовом пространстве, но можно передать токен вместе с приставкой /test. Класс telegram.Bot формирует базовый путь сложением base_url и token.
# content of /telegram/_bot.py self._base_url: str = base_url + self._token self._base_file_url: str = base_file_url + self._tokenПерехватывание исключений
По умолчанию все необработанные исключение в PTB пишутся в лог. Но при запуске тестов мы хотим убедиться, что в приложении не возникало ошибок. Для этого добавим обработчик ошибок, где будем сохранять возникшие исключения в отдельное место (в моем случае это переменная класса). Позже мы сможем проверить на наличие непредвиденных исключений.
async def collect_app_exceptions_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): self.collected_exception = context.error self.collection_event.set() # special asyncio event we are wating for while collecting replyes raise ApplicationHandlerStop # prevent any other error handlers app.add_error_handler(self.collect_app_exceptions_callback)
collection_event объект класса asyncio.Event — событие, исполнение которого мы ожидаем, пока ждем ответа от бота. В случае перехватывания исключений ждать ответа дальше смысла нет, так как произошла ошибка. Но pytest об этой ошибке ничего не знает, поэтому сохраним ее здесь и сделаем re-rise позже. Вы увидите инициализацию и полное применение collection_event в следующем блоке.
Отлично, наш бот работает. Теперь можем отправлять ему тестовые сообщения. Для этого понадобится аккаунт.
Тестовые номера
Для тестирования приложения мы будем использовать тестовые номера, которые предоставляет Telegram. Мы можем сформировать тестовые номера заранее и хранить их, к примеру, в .env файле или генерировать на лету для каждой тест-сессии отдельно. Тестовый номер — это:
99966XYYYY X — номер номер DC от 1 до 3 Y — любая цифра от 0 до 9
Тестовый номер позволяет в автоматическом режиме проходить авторизацию, и мы можем создать сколь угодно много аккаунтов. К примеру, мой бот позволяет давать другим людям доступ к общей базе фотографий. Так я смогу протестировать, что один клиент загрузил фотографию, другой получил к ней доступ, а третий нет.
Telegram периодически сбрасывает тестовые номера, поэтому все дальнейшие действия по авторизации и настройке telegram-клиента лучше производить непосредственно перед каждым запуском тестов.
Do not store any important or private information in the messages of such test accounts; anyone can make use of the simplified authorization mechanism – and we periodically wipe all information stored there. (© Telegram Documentation)
Создание клиента
Для упрощенной регистрации клиентов и перехватывания всех сообщений от бота отдельно для каждого клиента я инкапсулировал всю логику в отдельный класс ClientIntegration. Но в примерах здесь я удалил все опциональные настройки, чтобы не отвлекаться от сути. Код вспомогательного класса целиком можно посмотреть тут.
self.client = Client( ‘test-client’, api_id='<api_id>’, api_hash='<api_hash>’, test_mode=True, in_memory=True, phone_number=’99966′ + ‘1’ + ‘2023’, phone_code=’1′ * 5, )
def mock_input_callback(prompt: str = »): if ‘Enter first name: ‘ == prompt: return ‘<first name>’ if ‘Enter last name (empty to skip): ‘ == prompt: return ‘<last name>’ raise ValueError(prompt) def mock_print_callback(self, *args, **kwargs): … with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setattr(builtins, ‘input’, mock_input_callback) monkeypatch.setattr(builtins, ‘print’, mock_print_callback) …
@asynccontextmanager async def session_context(self) … try: # Some phonenumbers are registered already, some other not. # To be sure, handle sign up action by patching stdin/stdout. with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setattr(builtins, ‘input’, mock_input_callback) await self.client.start() # Update Telegram User properties, if they are needed for tests cases. await self.client.update_profile(‘<first name>’, ‘<last name>’, ‘<bio>’) if self.client.me.username != self.credits.username: await self.client.set_username(‘<username>’) # Clear messages to make each test isolated from others. await client.invoke( DeleteHistory(peer=await client.resolve_peer(‘<bot name>’), max_id=0, just_clear=False) ) yield self finally: await self.client.set_username(None) # cleaning up await self.client.stop()
Если для авторизации клиента использовать случайно сгенерированный тестовый номер, то стоит учесть, что другой разработчик мог установить двухфакторную авторизацию для этого номера, защитив его паролем. В таком случае остается только попробовать любой другой номер.
4. В итоге можем использовать ClientIntegration.session_context для описания фикстур.
@pytest.fixture(scope=’session’) async def integration_1(): async with ClientIntegration().session_context() as integration_1: yield integration_1 @pytest.fixture(scope=’session’) async def integration_2(): async with ClientIntegration().session_context() as integration_2: yield integration_2 …
Я использую параметр scope=’session’, чтобы инициализировать клиента один раз для всей сессии. Это сократит время на подготовку фикстур, а поскольку сам клиент не хранит данные, это не нарушит изолированности тестов.
Сборщик ответов
Для тестов необходимо отправлять сообщения боту и получать от него ответы. Если с первым все понятно, то для второго нужно повесить обработчик сообщений, где мы будем складывать все полученные сообщения в список, чтобы потом сделать assert на содержание полученных сообщений.
Но как долго нам ждать ответа от бота? Можно задать максимальное значение timeout, а можно указать конкретное количество. К примеру, сейчас ожидаем получить только одно сообщение, и как только оно будет получено, можно двигаться дальше.
# messages collector: async def collect_replyes_callback(self, client: Client, message: Message) self.collected_replyes.append(message) # If True, set event flag to True. No more waiting for messages: if len(self.collected_replyes) == self.collection_required_amount: self.collection_event.set() # timeout checker: async def collection_max_timeout_waiting(self, timeout: float): await sleep(timeout) self.collection_event.set() # apply handler and create Event: self.client.on_message(filters.chat(‘<bot name>’))(self.collect_replyes_callback) self.collection_event = asyncio.Event() # Wait until all messages are received or reaching timeout. timout_task = asyncio.create_task(self.collection_max_timeout_waiting(timeout)) await self.collection_event.wait() timout_task.cansel()
Теперь мы можем проверить полученные сообщения и то, что в приложении не было ошибок (мы использовали error_handler при инициализации приложения PTB в предыдущем блоке).
if self.collected_exceptions: raise self.collected_exception assert len(self.collected_replyes) == amount, ‘Received unexpected messages amount. ‘
Для удобства я описал все эти действия в отдельном методе ClientIntegration и также обернул его в @asynccontextmanager. При выходе из контекстного менеджера collect сработает блок finally, где мы будем ожидать ответы от бота.
@asynccontextmanager async def collect(self, *, amount: int, timeout: float = 2.0): self.collection_amount = amount self.collected_replyes.clear() self.collection_event = asyncio.Event() try: yield self.collected_replyes finally: timout_event = asyncio.create_task(self.collection_max_timeout_waiting(timeout)) await self.collection_event.wait() timout_event.cancel() if self.collected_exceptions: raise self.collected_exception assert len(self.collected_replyes) == amount, ‘Received unexpected messages amount. ‘
К примеру, мы хотим протестировать отправку сообщения /start и получение ответа Hello, {username}!. Благодаря фикстурам, описанным выше, этот тест-кейс может быть описан буквально тремя строчками.
pytestmark = [ pytest.mark.anyio, pytest.mark.usefixtures(‘application’) # PTB Application ] async def test_start_handler(integration: ClientIntegration): # test action: async with integration.collect(amount=1) as replyes: await integration.client.send_message(‘<bot name>’, ‘/start’) # at __aexit__ wait until all messages are received # test assertion: assert replyes[0].text == f’Hellow, {username}!’
В качестве альтернативы Pyrogram может быть Telethon. Это схожая библиотека для работы с клиентом Telegram. Из плюсов — Telethon реализует метод conversation, что упрощает сборку ответов клиенту от бота. Подробнее можно посмотреть тут.
with client.conversation(‘<bot name>’) as conv: await conv.send_message(«/start») reply: Message = await conv.get_response() assert reply.raw_text == f’Hellow, {username}!’
Потратив дополнительное время на подготовку фикстур, мы получаем все необходимые инструменты для написания тестов от начала и до конца. Мы можем создать несколько пользователей, отправлять им разные тестовые сообщения и проверять ответы. Это удобно не только для написания тестов в целом, но и во время разработки — как точка входа для запуска и отладки. Вместо того чтобы каждый раз доставать телефон и вручную отправлять сообщения боту, можно описать простой тест на отправку сообщения клиентом.
Но стоит понимать, что такой способ тестирования не самый дешевый с точки зрения реализации, поддержки и времени исполнения самих тестов. Каждый запрос на регистрацию клиента, отправку сообщения клиентом, на отправку ответа ботом происходит посредством реальных запрос к серверу Telegram. А это занимает время. Когда подобных тестов станет много, это может стать проблемой. Вот почему для написания тестов стоит придерживаться парадигмы Testing Pyramid и реализовывать подобным методом только базовые и важные функции вашего приложения.
Привет, Хабр. Я Михаил Выборный, python-разработчик, backend-developer в облачном провайдере beeline cloud. В этой статье я хочу поделиться опытом написания автоматизированных end-to-end-тестов без эмуляции Telegram Bot API, но с использованием тестовых аккаунтов. Мы зайдем в изолированное тестовое пространство Telegram, создадим тестового бота, подготовим фикстуру для запуска нашего приложения и напишем авторизацию для тестовых клиентов.
По ходу статьи я буду использовать инструменты следующих библиотек:
Python Telegram Bot — для написания бота (сокращенно PTB);
Pytest — для организации тестов;
Anyio — для асинхронных тестов и фикстур;
Pyrogram — для отправки тестовых сообщений;
asyncio.Event — для оповещения о получении ожидаемых сообщений и предотвращения излишних ожиданий;
contexlib — для удобного синтаксиса написания контекстных менеджеров.
Статья написана на примере реализации несложного бота для хранения фотографий и отправки их в ответ при запросе. Вы можете посмотреть исходный код приложения и тестов на GitHub. Ниже я использую короткие выдержки из официальной документации Telegram на английском – к ним даю пояснения на русском своими словами.
Альтернативы
Перед тем как что-то писать самому, я пытался найти готовое решение и был уверен, что Google мне поможет. Но оказалось, что готового оптимального решения просто не существует. Здесь я кратко перечислю то, что мне удалось найти и почему это не подошло.
PTB Test
больше не поддерживается
интеграция с unittest (не pytest)
Aiogram Tests
подходит для тестирования ботов, написанных только на aiogram
реализует unit-тестирование через Mock-объекты (без интеграции Telegram Bot API)
Все тест-кейсы описываются как вызов конкретных handler-ов, из-за чего нет возможности протестировать их поведения целиком — от отправки пользователем сообщения до получения ответа. К примеру, непонятно, как тестировать фильтрацию входящих сообщений и их распределение по различным handler-ам и т. п.
Tg Integration
также не поддерживается
тянет устаревшие зависимости (pyrogram < 2.0.0 typing-extensions < 4.0.0)
практически отсутствует документация
Перед тем как начать. Регистрация APP
Предположим, наш бот уже готов и мы переходим к тестированию. Для этого мы подготовим необходимые инструменты, чтобы написание самих тестов было кратким и понятным. Отправить сообщение, получить ответ, проверить его. Да, придется потратить время, чтобы реализовать вспомогательные фикстуры по регистрации клиентов и обработки входящих сообщений. Но, заморочившись однажды, мы сможем писать сами тесты всего в несколько строк. Их будет легко обновлять и поддерживать.
Отправлять тестовые сообщения мы будем с помощью Pyrogram. Для этого нужно зарегистрировать будущее клиентское приложение.
После регистрации мы получаем api_key и api_id, которые можем использовать для создания множества клиентских приложений.
The API key defines a token for a Telegram application you are going to build. This means that you are able to authorize multiple users or bots with a single API key. (© Telegram Documentation)
Тестовый бот
Регистрация бота
Для тестирования пользовательских приложений Telegram дает доступ к Dedicated test environment (выделенная тестовая среда). Это изолированная песочница, где можно регистрировать пользователей и создавать ботов. Если аккаунты мы будем создавать динамически, используя тестовые номера, то зарегистрировать тестового бота у @BotFather проще вручную, используя свой настоящий номер.
The test environment is completely separate from the main environment, so you will need to create a new user account and a new bot with @BotFather. (© Telegram Documentation)
Как это сделать, можно прочитать тут. Я использовал приложение Telegram для iOS, кликнув 10 раз на иконку Settings (Настройки).
После этого в приложении становится доступен второй аккаунт. Это очень удобное свойство Telegram, о котором знают немногие (лично я раньше не знал). Можно использовать для различного тестирования или даже завести секретный чат с другим человеком (который тоже зарегистрировался в тестовом пространстве). Далее, как обычно, регистрируем бота у @BotFather и получаем токен. Для обращения к любому из методов нужно добавить /test/ в путь запроса.
You do not have permission to view link please Вход or Регистрация
<token>/test/METHOD_NAMEЗапуск бота во время тестовТеперь при запуске тестов мы можем запустить наше приложение в тестовом пространстве. Для этого подготовим фикстуру.
@pytest.fixture(scope=’session’) # fixture runs Bot app only once for entire tess session async def application(): app = Application.builder().token(‘<token>’ + ‘/test’).build() await app.initialize() await app.post_init(app) # initialize does *not* call `post_init` — that is only done by run_polling/webhook await app.start() await app.updater.start_polling() yield app await app.updater.stop() await app.stop() await app.shutdown()
Обычно для запуска приложения мы используем app.run_polling(), но это заблокирует дальнейшее исполнение программы. Мы же будем запускать тесты в параллель с тем, как работает наш бот. Подробнее о том, как запускать PTB с другим асинхронным кодом, — Running PTB alongside other asyncio frameworks
# content of /telegram/_bot.py self._base_url: str = base_url + self._token self._base_file_url: str = base_file_url + self._tokenПерехватывание исключений
По умолчанию все необработанные исключение в PTB пишутся в лог. Но при запуске тестов мы хотим убедиться, что в приложении не возникало ошибок. Для этого добавим обработчик ошибок, где будем сохранять возникшие исключения в отдельное место (в моем случае это переменная класса). Позже мы сможем проверить на наличие непредвиденных исключений.
async def collect_app_exceptions_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): self.collected_exception = context.error self.collection_event.set() # special asyncio event we are wating for while collecting replyes raise ApplicationHandlerStop # prevent any other error handlers app.add_error_handler(self.collect_app_exceptions_callback)
Отлично, наш бот работает. Теперь можем отправлять ему тестовые сообщения. Для этого понадобится аккаунт.
Тестовый клиент-аккаунт
Тестовые номера
Для тестирования приложения мы будем использовать тестовые номера, которые предоставляет Telegram. Мы можем сформировать тестовые номера заранее и хранить их, к примеру, в .env файле или генерировать на лету для каждой тест-сессии отдельно. Тестовый номер — это:
99966XYYYY X — номер номер DC от 1 до 3 Y — любая цифра от 0 до 9
Тестовый номер позволяет в автоматическом режиме проходить авторизацию, и мы можем создать сколь угодно много аккаунтов. К примеру, мой бот позволяет давать другим людям доступ к общей базе фотографий. Так я смогу протестировать, что один клиент загрузил фотографию, другой получил к ней доступ, а третий нет.
Do not store any important or private information in the messages of such test accounts; anyone can make use of the simplified authorization mechanism – and we periodically wipe all information stored there. (© Telegram Documentation)
Создание клиента
Для упрощенной регистрации клиентов и перехватывания всех сообщений от бота отдельно для каждого клиента я инкапсулировал всю логику в отдельный класс ClientIntegration. Но в примерах здесь я удалил все опциональные настройки, чтобы не отвлекаться от сути. Код вспомогательного класса целиком можно посмотреть тут.
Создадим клиента. В отличие от PTB, у Pyrogram есть нативная поддержка тестового пространства Telegram.
self.client = Client( ‘test-client’, api_id='<api_id>’, api_hash='<api_hash>’, test_mode=True, in_memory=True, phone_number=’99966′ + ‘1’ + ‘2023’, phone_code=’1′ * 5, )
in_memory — после авторизации сессия не будет записываться в файл test-client.session, а останется в памяти. Это позволит изолировать тесты друг от друга и не подчищать *.session файлы вручную после тестов.
phone_code — код подтверждения авторизации. Для тестовых номеров всегда равен номеру DC х 5 раз.
Мы уже указали телефонный номер и код подтверждения. Но может быть такое, что тестовый номер не зарегистрирован. В этом случае Pyrogram попросит ввести имя и фамилию. Чтобы пройти процесс регистрации автоматически, заменим stdout и stdin:
def mock_input_callback(prompt: str = »): if ‘Enter first name: ‘ == prompt: return ‘<first name>’ if ‘Enter last name (empty to skip): ‘ == prompt: return ‘<last name>’ raise ValueError(prompt) def mock_print_callback(self, *args, **kwargs): … with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setattr(builtins, ‘input’, mock_input_callback) monkeypatch.setattr(builtins, ‘print’, mock_print_callback) …
Теперь мы можем запустить приложение клиента Pyrogram. Для удобства я описал все действия в отдельном методе ClientIntegration и обернул его в asynccontextmanager. Я использую здесь контекст-менеджер, чтобы явно описать setup и teardown. Позже это будет удобно интегрировать в Pytest в качестве yield фикстуры.
@asynccontextmanager async def session_context(self) … try: # Some phonenumbers are registered already, some other not. # To be sure, handle sign up action by patching stdin/stdout. with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setattr(builtins, ‘input’, mock_input_callback) await self.client.start() # Update Telegram User properties, if they are needed for tests cases. await self.client.update_profile(‘<first name>’, ‘<last name>’, ‘<bio>’) if self.client.me.username != self.credits.username: await self.client.set_username(‘<username>’) # Clear messages to make each test isolated from others. await client.invoke( DeleteHistory(peer=await client.resolve_peer(‘<bot name>’), max_id=0, just_clear=False) ) yield self finally: await self.client.set_username(None) # cleaning up await self.client.stop()
4. В итоге можем использовать ClientIntegration.session_context для описания фикстур.
@pytest.fixture(scope=’session’) async def integration_1(): async with ClientIntegration().session_context() as integration_1: yield integration_1 @pytest.fixture(scope=’session’) async def integration_2(): async with ClientIntegration().session_context() as integration_2: yield integration_2 …
Тестирование
Сборщик ответов
Для тестов необходимо отправлять сообщения боту и получать от него ответы. Если с первым все понятно, то для второго нужно повесить обработчик сообщений, где мы будем складывать все полученные сообщения в список, чтобы потом сделать assert на содержание полученных сообщений.
Но как долго нам ждать ответа от бота? Можно задать максимальное значение timeout, а можно указать конкретное количество. К примеру, сейчас ожидаем получить только одно сообщение, и как только оно будет получено, можно двигаться дальше.
# messages collector: async def collect_replyes_callback(self, client: Client, message: Message) self.collected_replyes.append(message) # If True, set event flag to True. No more waiting for messages: if len(self.collected_replyes) == self.collection_required_amount: self.collection_event.set() # timeout checker: async def collection_max_timeout_waiting(self, timeout: float): await sleep(timeout) self.collection_event.set() # apply handler and create Event: self.client.on_message(filters.chat(‘<bot name>’))(self.collect_replyes_callback) self.collection_event = asyncio.Event() # Wait until all messages are received or reaching timeout. timout_task = asyncio.create_task(self.collection_max_timeout_waiting(timeout)) await self.collection_event.wait() timout_task.cansel()
Теперь мы можем проверить полученные сообщения и то, что в приложении не было ошибок (мы использовали error_handler при инициализации приложения PTB в предыдущем блоке).
if self.collected_exceptions: raise self.collected_exception assert len(self.collected_replyes) == amount, ‘Received unexpected messages amount. ‘
Для удобства я описал все эти действия в отдельном методе ClientIntegration и также обернул его в @asynccontextmanager. При выходе из контекстного менеджера collect сработает блок finally, где мы будем ожидать ответы от бота.
@asynccontextmanager async def collect(self, *, amount: int, timeout: float = 2.0): self.collection_amount = amount self.collected_replyes.clear() self.collection_event = asyncio.Event() try: yield self.collected_replyes finally: timout_event = asyncio.create_task(self.collection_max_timeout_waiting(timeout)) await self.collection_event.wait() timout_event.cancel() if self.collected_exceptions: raise self.collected_exception assert len(self.collected_replyes) == amount, ‘Received unexpected messages amount. ‘
Описание тестов
К примеру, мы хотим протестировать отправку сообщения /start и получение ответа Hello, {username}!. Благодаря фикстурам, описанным выше, этот тест-кейс может быть описан буквально тремя строчками.
pytestmark = [ pytest.mark.anyio, pytest.mark.usefixtures(‘application’) # PTB Application ] async def test_start_handler(integration: ClientIntegration): # test action: async with integration.collect(amount=1) as replyes: await integration.client.send_message(‘<bot name>’, ‘/start’) # at __aexit__ wait until all messages are received # test assertion: assert replyes[0].text == f’Hellow, {username}!’
with client.conversation(‘<bot name>’) as conv: await conv.send_message(«/start») reply: Message = await conv.get_response() assert reply.raw_text == f’Hellow, {username}!’
Заключение
Потратив дополнительное время на подготовку фикстур, мы получаем все необходимые инструменты для написания тестов от начала и до конца. Мы можем создать несколько пользователей, отправлять им разные тестовые сообщения и проверять ответы. Это удобно не только для написания тестов в целом, но и во время разработки — как точка входа для запуска и отладки. Вместо того чтобы каждый раз доставать телефон и вручную отправлять сообщения боту, можно описать простой тест на отправку сообщения клиентом.
Но стоит понимать, что такой способ тестирования не самый дешевый с точки зрения реализации, поддержки и времени исполнения самих тестов. Каждый запрос на регистрацию клиента, отправку сообщения клиентом, на отправку ответа ботом происходит посредством реальных запрос к серверу Telegram. А это занимает время. Когда подобных тестов станет много, это может стать проблемой. Вот почему для написания тестов стоит придерживаться парадигмы Testing Pyramid и реализовывать подобным методом только базовые и важные функции вашего приложения.
beeline cloud — secure cloud provider.
Разрабатываем облачные решения, чтобы вы предоставляли клиентам лучшие сервисы