pytest-asyncio tests not running async - pytest

I'm trying to implement a simple async test suite. If my understanding is correct of async, the tests below should only take about 2 seconds to run. However, it's taking 6 seconds. What am I missing to make these test to run async ("at the same time")?
import logging
import pytest
import asyncio
MSG_FORMAT = "%(asctime)s.%(msecs)03d %(module)s->%(funcName)-15s |%(levelname)s| %(message)s"
MSG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
LOG_LEVEL = logging.INFO
# Create logger
logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
# Create Stream Handler
log_stream = logging.StreamHandler()
log_format = logging.Formatter(fmt=MSG_FORMAT, datefmt=MSG_DATE_FORMAT)
log_stream.setFormatter(log_format)
log_stream.setLevel(LOG_LEVEL)
logger.addHandler(log_stream)
class TestMyStuff:
#staticmethod
async def foo(seconds):
await asyncio.sleep(seconds)
return 1
#pytest.mark.asyncio
async def test_1(self, event_loop):
logger.info("start")
assert await event_loop.create_task(self.foo(2)) == 1
logger.info("end")
#pytest.mark.asyncio
async def test_2(self, event_loop):
logger.info("start")
assert await event_loop.create_task(self.foo(2)) == 1
logger.info("end")
#pytest.mark.asyncio
async def test_3(self, event_loop):
logger.info("start")
# assert await event_loop.run_in_executor(None, self.foo) == 1
assert await event_loop.create_task(self.foo(2)) == 1
logger.info("end")
pytest extras:
plugins: asyncio-0.18.3, aiohttp-1.0.4

pytest-asyncio runs asynchronous tests serially. That plugin's goal is to make testing asynchronous code more convenient.
pytest-asyncio-cooperative on the other hand has the goal of running asyncio tests concurrently via cooperative multitasking (ie. all async coroutines sharing the same event loop and yielding to each other).
To try out pytest-asyncio-cooperative do the following:
Install the plugin
pip install pytest-asyncio-cooperative
Replace the #pytest.mark.asyncio marks with #pytest.mark.asyncio_cooperative
Remove all references to event_loop. pytest-asyncio-cooperative uses a single implicit event loop for asyncio interactions.
Run pytest with pytest-asyncio disabled. It is not compatible with pytest-asyncio-cooperative
pytest -p no:asyncio test_mytestfile.py
Here is the original code snippet with these modifications:
import logging
import pytest
import asyncio
MSG_FORMAT = "%(asctime)s.%(msecs)03d %(module)s->%(funcName)-15s |%(levelname)s| %(message)s"
MSG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
LOG_LEVEL = logging.INFO
# Create logger
logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
# Create Stream Handler
log_stream = logging.StreamHandler()
log_format = logging.Formatter(fmt=MSG_FORMAT, datefmt=MSG_DATE_FORMAT)
log_stream.setFormatter(log_format)
log_stream.setLevel(LOG_LEVEL)
logger.addHandler(log_stream)
class TestMyStuff:
#staticmethod
async def foo(seconds):
await asyncio.sleep(seconds)
return 1
#pytest.mark.asyncio_cooperative
async def test_1(self):
logger.info("start")
assert await self.foo(2) == 1
logger.info("end")
#pytest.mark.asyncio_cooperative
async def test_2(self):
logger.info("start")
assert await self.foo(2) == 1
logger.info("end")
#pytest.mark.asyncio_cooperative
async def test_3(self):
logger.info("start")
# assert await event_loop.run_in_executor(None, self.foo) == 1
assert await self.foo(2) == 1
logger.info("end")
And here are the test results:
plugins: hypothesis-6.39.4, asyncio-cooperative-0.28.0, anyio-3.4.0, typeguard-2.12.1, Faker-8.1.0
collected 3 items
test_mytestfile.py ... [100%]
================================ 3 passed in 2.18s =================================
Please checkout the README of pytest-asyncio-cooperative. Now that tests are run in a concurrent way you need to be wary of shared resources (eg. mocking)

Related

Update on sqlalchemy +asyncpg , returns another operation in progress on pytest

I just updated my app with sqlalchemy to create an async connections, no problem, but when writing tests, I can't do the update as opposed to create:
affected function:
#function
#classmethod
async def update_instance(
cls,
instance: InstanceInputOnUpdate
) -> Union['EdaNCEInstance', EdaNCEInstanceOperationError]:
async with get_session() as conn:
result = await conn.execute(
select(Instance).where(Instance.id==eda_nce_instance.id)
)
instance_to_update = result.scalars().unique().first()
if instance_to_update is not None:
instance_to_update.name = instance.name
instance_to_update.host = instance.host
await conn.commit()
return instance_to_update
else:
return InstanceOperationError(
result= False,
message= f"Can't find the instance ID '{instance.id}'"
)
test:
#pytest.mark.asyncio
async def test_04_update_instance():
async with AsyncClient(app=app, base_url="http://test") as c:
res = await Instance.update_eda_nce_instance(
instance=InstanceInputOnUpdate(
id=DUMMY_ID,
name="test_server1",
host="123.123.123.123",
)
)
assert isinstance(res, Instance) == True
assert res.host == "124.123.144.144"
return res
errors:
#...
E sqlalchemy.dialects.postgresql.asyncpg.AsyncAdapt_asyncpg_dbapi.InterfaceError: <class 'asyncpg.exceptions._base.InterfaceError'>: cannot perform operation: another operation is in progress
venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py:682: InterfaceError
my db.py
SQLALCHEMY_DATABASE_URL = settings.get_settings().postgresql_conn_url
engine = create_async_engine(SQLALCHEMY_DATABASE_URL, echo=False, future=True)
async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
Base = declarative_base()
async def init_db():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
#asynccontextmanager
async def get_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
async with session.begin():
try:
yield session
finally:
await session.close()
In creation I have no problem, and it doesn't change much from the update
#classmethod
async def create_instance(
cls,
instance: InstanceInputOnCreate
) -> Union['Instance', InstanceOperationError]:
async with get_session() as conn:
new_instance = Instance(
name=instance.name,
host=instance.host
)
conn.add(new_instance)
try:
await conn.commit()
return new_instance
except Exception:
conn.rollback()
return InstanceOperationError(
result=False,
message=f"Instance '{instance.name}' already exists"
)
I didn't create mocks, because CI/CDs are set up to create a test database where Alembic can be started first for migrations, then the database is ready to test on it
sqlalchemy==1.4.46
fastapi-utils==0.2.1
aiosqlite==0.18.0
asyncpg==0.27.0
sqlalchemy-utils==0.39.0
pytest==7.2.1
pytest-asyncio==0.20.3
pytest-mock==3.10.0
pydantic~=1.10.4
aiokafka~=0.8.0
requests~=2.28.1
fastapi==0.70.1
If there are typo in the code, it's not a problem I had to change the names to create this question.

Running an asyncoio socket server and client on the same process for tests

I wrote a socket server and client package with asyncio that happens to be a single module with two classes Server and Client. The intention is to have the code check if the specified port is in use on the same device, and if so, it will choose the use the Client class instead of the Server class. I am doing this to enable local testing between client and server for a larger goal.
I wrote my unit test with pytest and I can get it to pass if I run the server in its own process first, then run the client in another process.
However, I want to test both server and client together in the same process using pytest. The test never completes this way, however.
Am I running into an issue with asyncio here? Or is it an issue with pytest? Or is it something I am doing wrong in my code?
WebSockets.websocket
import asyncio
class Server:
def __init__(self, host, port):
self.host = host
self.port = port
self.server = None
self.connections = {}
async def start(self):
self.server = server = await asyncio.start_server(self.handle_client, self.host, self.port)
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await server.serve_forever()
async def handle_client(self, reader, writer):
addr = writer.get_extra_info('peername')
print(f'New connection from {addr}')
self.connections[addr] = writer
try:
while not reader.at_eof():
data = await reader.read(100)
message = data.decode()
if message:
print(f'Received {message!r} from {addr}')
await self.send(message, addr)
finally:
del self.connections[addr]
writer.close()
async def send(self, message, addr):
writer = self.connections.get(addr)
if not writer:
return
writer.write(message.encode())
await writer.drain()
async def recv(self, addr):
reader, _ = await asyncio.open_connection(addr[0], addr[1])
data = await reader.read(100)
return data.decode()
async def stop(self):
await self.server.close()
class Client:
def __init__(self, host, port):
self.host = host
self.port = port
self.reader = None
self.writer = None
async def connect(self):
self.reader, self.writer = await asyncio.open_connection(self.host, self.port)
async def send(self, message):
self.writer.write(message.encode())
async def recv(self):
data = await self.reader.read(100)
return data.decode()
async def close(self):
self.writer.close()
Test
import socket
import asyncio
from WebSockets.websocket import Server, Client
import pytest
def check_port(address, port):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((address, port))
s.close()
return "server"
except OSError:
return "client"
async def serve():
server = Server('localhost', 8910)
await server.start()
async def client_connect():
client = Client('localhost', 8910)
await client.connect()
await client.send("Hello world!")
message = await client.recv()
if message:
print(f"Received {message!r} from server")
assert message == "Hello world!"
await client.close()
#pytest.mark.asyncio
async def test_sockets():
if check_port("localhost", 8910) == "server":
await serve()
# for reference on how to use in non pytest module
# loop = asyncio.new_event_loop()
# loop.run_until_complete(serve())
# loop.close()
else:
await client_connect()
# for reference on how to use in non pytest module
# loop = asyncio.new_event_loop()
# loop.run_until_complete(client_connect())
# loop.close()

How can I de-duplicate responses for pytest?

The responses library provides mocks for requests. In my case, it looks typically like this:
import responses
#responses.activate
def test_foo():
# Add mocks for service A
responses.add(responses.POST, 'http://service-A/foo', json={'bar': 'baz'}, status=200)
responses.add(responses.POST, 'http://service-A/abc', json={'de': 'fg'}, status=200)
#responses.activate
def test_another_foo():
# Add mocks for service A
responses.add(responses.POST, 'http://service-A/foo', json={'bar': 'baz'}, status=200)
responses.add(responses.POST, 'http://service-A/abc', json={'de': 'fg'}, status=200)
How can I avoid this code duplication?
I would love to have a mock_service_a fixture or something similar.
Just as you suggest, creating a fixture solves these issues.
import pytest
import responses
import requests
#pytest.fixture(scope="module", autouse=True)
def mocked_responses():
with responses.RequestsMock() as rsps:
rsps.add(
responses.POST, "http://service-a/foo", json={"bar": "baz"}, status=200
)
rsps.add(
responses.POST, "http://service-a/abc", json={"de": "fg"}, status=200
)
yield rsps
def test_foo():
resp = requests.post("http://service-a/foo", json={"bar": "baz"})
assert resp.status_code == 200
def test_another_foo():
resp = requests.post("http://service-a/abc", json={"de": "fg"})
assert resp.status_code == 200
Running it returns:
==================================== test session starts =====================================
platform darwin -- Python 3.9.1, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: **
collected 2 items
tests/test_grab.py .. [100%]
===================================== 2 passed in 0.21s ======================================

Run pytest inside a script (if name...)

I have the next file with tests
import pytest
from httpx import AsyncClient
import sys
import config
from main import app
#pytest.mark.asyncio
async def test_register():
async with AsyncClient(app=app, base_url="http://test") as ac:
response = await ac.post("/register", )
assert response.status_code == 200
I want to run it like
if __name__ == '__main__':
pytest.run() # Or something alike
How I can do it? I need it to assign name 'main' to this module, because the main module (which import above) has a constraint like: if __name__ == '__main__', so without it tests will not be run indeed.
pytest.main() would run pytest in the current working directory and this would include your file depending on the filename (e.g. if the filename starts with test_). To run pytest on the current file only you can run:
if __name__ == "__main__":
pytest.main([__file__])

How to use the fixture 'test_client in pytest-aiohttp

there is an elementary test
from aiohttp import web
async def hello(request):
return web.Response(text='Hello, world')
async def test_hello(test_client, loop):
app = web.Application()
app.router.add_get('/', hello)
client = await test_client(app)
resp = await client.get('/')
assert resp.status == 200
text = await resp.text()
assert 'Hello, world' in text
fixture 'test_client' not found
available fixtures: cache, capfd, capsys, doctest_namespace, event_loop, event_loop_process_pool, loop, monkeypatch,
pytestconfig, record_xml_property, recwarn, tmpdir, tmpdir_factory, unused_tcp_port, unused_tcp_port_factory
You need to install pytest-aiohttp plugin.
pip install pytest-aiohttp
It is described in very begin of testing chapter in aiohttp documentation.