Description
Discussed in #587
Originally posted by seifertm July 18, 2023
Motivation
pytest-asyncio provides an asyncio event loop via the event_loop fixture. The fixture is function-scoped by default. This is useful for unit testing as it ensures a high level of isolation between test cases. However, as soon as users want to write larger tests that span multiple test cases the scope of the event_loop fixture needs to be extended.
Extending the scope of the event_loop fixture is currently achieved by reimplementing the fixture with an appropriate scope. This approach leads to code duplication, because users are forced to implement the fixture body over and over again. Users are also known to modify the fixture implementation and extend it with custom setup or teardown code.
Several problems arise from this: For one, changes to the fixture implementation in pytest-asyncio would require all users to change their fixture implementations accordingly. For another, requiring users to implement their own event_loop fixture somewhat defeats the purpose of pytest-asyncio in the first place.
Proposed solution
Pytest nodes are discovered by a hierarchy of collectors. Pytest marks can be located at different levels of this hierarchy (see Marking whole classes or modules).
The proposed solution is to derive the scope of the asyncio event loop from the location of the asyncio
mark. Consider the following examples.
Strict mode
Test functions
@pytest.mark.asyncio
async def test_a():
...
@pytest.mark.asyncio
async def test_b():
...
Each function test_a and test_b have their own asyncio mark. Therefore, the event loop scope will be limited to each test function.
Test classes
When using test classes the scope of the event loop can be per-class or per-function, depending on the location of
the asyncio
mark.
class MyTestsInFunctionScopedLoop:
@pytest.mark.asyncio
async def test_a(self):
...
@pytest.mark.asyncio
async def test_b(self):
...
The functions test_a and test_b each run in their own loop, because the asyncio marker is directly attached to them.
@pytest.mark.asyncio
class MyTestsInClassScopedLoop:
async def test_a(self):
...
async def test_b(self):
...
The functions test_a and test_b run in a common loop, because the asyncio marker is attached to the class.
Module-scoped loops
If the entire module has the asyncio
mark all tests are run in a module-scoped event loop.
pytestmark = pytest.mark.asyncio
async def test_a():
...
class MyTests:
async def test_b(self):
...
Auto mode
Function scope
async def test_a():
...
Class scope
@pytest.mark.asyncio
class MyTests:
async def test_a(self):
...
Module scope
pytestmark = pytest.mark.asyncio
async def test_a(self):
...
Session scope
pytestmark = pytest.mark.asyncio # in root conftest.py
async def test_a(self):
...
Considered alternatives
Dynamic fixture scopes
Introducing dynamic fixture scope in pytest is more effort compared to the proposed solution as it introduces new features to pytest.