1

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

enter image description here

efe_man
  • 21
  • 3
  • 1
    The pytest-asyncio plugin doesn’t run all of your tests concurrently. They continue to run one at a time, but each marked test is scheduled with its own event loop. – dirn Jun 17 '22 at 00:43
  • 1
    You might be interested in the [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) plugin. – dirn Jun 17 '22 at 00:45
  • Unfortunately, pytest-xdist is what I’m trying to avoid. I’m wondering if I could use the pytest collection hooks to run the test functions “manually” in a single event loop. – efe_man Jun 17 '22 at 03:16

1 Answers1

1

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:

  1. Install the plugin
pip install pytest-asyncio-cooperative
  1. Replace the @pytest.mark.asyncio marks with @pytest.mark.asyncio_cooperative

  2. Remove all references to event_loop. pytest-asyncio-cooperative uses a single implicit event loop for asyncio interactions.

  3. 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)

likeaneel
  • 83
  • 6