AttributeError in pytest with asyncio after include code in fixtures
Asked Answered
P

3

19

I need to test my telegram bot. To do this I need to create client user to ask my bot. I found telethon library which can do it. First I wrote a code example to ensure that authorisation and connection works and send test message to myself (imports omitted):

api_id = int(os.getenv("TELEGRAM_APP_ID"))
api_hash = os.getenv("TELEGRAM_APP_HASH")
session_str = os.getenv("TELETHON_SESSION")

async def main():
    client = TelegramClient(
        StringSession(session_str), api_id, api_hash,
        sequential_updates=True
    )
    await client.connect()
    async with client.conversation("@someuser") as conv:
        await conv.send_message('Hey, what is your name?')


if __name__ == "__main__":
    asyncio.run(main())

@someuser (me) successfully receives message. Okay, now I create a test with fixtures based on code above:

api_id = int(os.getenv("TELEGRAM_APP_ID"))
api_hash = os.getenv("TELEGRAM_APP_HASH")
session_str = os.getenv("TELETHON_SESSION")

@pytest.fixture(scope="session")
async def client():
    client = TelegramClient(
        StringSession(session_str), api_id, api_hash,
        sequential_updates=True
    )
    await client.connect()
    yield client
    await client.disconnect()


@pytest.mark.asyncio
async def test_start(client: TelegramClient):
    async with client.conversation("@someuser") as conv:
        await conv.send_message("Hey, what is your name?")

After running pytest received an error:

AttributeError: 'async_generator' object has no attribute 'conversation'

It seems client object returned from client fixture in "wrong" condition. Here is print(dir(client)):

['__aiter__', '__anext__', '__class__', '__class_getitem__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'aclose', 'ag_await', 'ag_code', 'ag_frame', 'ag_running', 'asend', 'athrow']

Where I loose "right" client object from generator in fixture?

Pagel answered 15/7, 2022 at 16:7 Comment(0)
T
41

Use @pytest_asyncio.fixture decorator in async fixtures according to documentation https://pypi.org/project/pytest-asyncio/#async-fixtures.

Like this:

import pytest_asyncio

@pytest_asyncio.fixture(scope="session")
async def client():
    ...
Tother answered 18/7, 2022 at 8:11 Comment(0)
J
7

I would rather use anyio instead of pytest-asyncio

  • pip install anyio

Try this:

import pytest

@pytest.fixture(scope="session")
def anyio_backend():
    return "asyncio"

@pytest.fixture(scope="session")
async def conv():
    client = TelegramClient(
        StringSession(session_str), api_id, api_hash,
        sequential_updates=True
    )
    await client.connect()
    async with client.conversation("@someuser") as conv:
        yield conv

@pytest.mark.anyio
async def test_start(conv):
    await conv.send_message("Hey, what is your name?")
Janiecejanifer answered 15/7, 2022 at 16:19 Comment(2)
Wow, it works. Thank you! Could you please explain, is it mean that pytest-asyncio is not suitable for this task? Is it something base understanding I should know as beginner developer, or its just peculiar properties/oddities pytest-asyncio plugin?Pagel
@Andrey I use pytest-asyncio at 2019-2021, but use anyio since this year. The reason is that we use fastapi for work and anyio is one of it's dependence.Janiecejanifer
C
5

You have two options:

  1. Set asyncio_mode to auto—see readthedocs and concepts—in which case you can omit @pytest.mark.asyncio and use @pytest.fixture for fixtures, OR
  2. Use @pytest_asyncio.fixture per Filip Hanes' answer

Option 1 is the easy way, but option 2 is available for those who want to run some async tests using custom async libraries (such as trio).

Cachucha answered 6/4, 2023 at 20:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.