diff --git a/docs/source/concepts.rst b/docs/source/concepts.rst index e774791e..710c5365 100644 --- a/docs/source/concepts.rst +++ b/docs/source/concepts.rst @@ -4,19 +4,43 @@ Concepts asyncio event loops =================== -pytest-asyncio runs each test item in its own asyncio event loop. The loop can be accessed via ``asyncio.get_running_loop()``. - -.. code-block:: python - - async def test_runs_in_a_loop(): - assert asyncio.get_running_loop() - -Synchronous test functions can get access to an asyncio event loop via the `event_loop` fixture. - -.. code-block:: python - - def test_can_access_current_loop(event_loop): - assert event_loop +In order to understand how pytest-asyncio works, it helps to understand how pytest collectors work. +If you already know about pytest collectors, please :ref:`skip ahead `. +Otherwise, continue reading. +Let's assume we have a test suite with a file named *test_all_the_things.py* holding a single test, async or not: + +.. include:: concepts_function_scope_example.py + :code: python + +The file *test_all_the_things.py* is a Python module with a Python test function. +When we run pytest, the test runner descends into Python packages, modules, and classes, in order to find all tests, regardless whether the tests will run or not. +This process is referred to as *test collection* by pytest. +In our particular example, pytest will find our test module and the test function. +We can visualize the collection result by running ``pytest --collect-only``:: + + + + +The example illustrates that the code of our test suite is hierarchical. +Pytest uses so called *collectors* for each level of the hierarchy. +Our contrived example test suite uses the *Module* and *Function* collectors, but real world test code may contain additional hierarchy levels via the *Package* or *Class* collectors. +There's also a special *Session* collector at the root of the hierarchy. +You may notice that the individual levels resemble the possible `scopes of a pytest fixture. `__ + +.. _pytest-asyncio-event-loops: + +Pytest-asyncio provides one asyncio event loop for each pytest collector. +By default, each test runs in the event loop provided by the *Function* collector, i.e. tests use the loop with the narrowest scope. +This gives the highest level of isolation between tests. +If two or more tests share a common ancestor collector, the tests can be configured to run in their ancestor's loop by passing the appropriate *scope* keyword argument to the *asyncio* mark. +For example, the following two tests use the asyncio event loop provided by the *Module* collector: + +.. include:: concepts_module_scope_example.py + :code: python + +It's highly recommended for neighboring tests to use the same event loop scope. +For example, all tests in a class or module should use the same scope. +Assigning neighboring tests to different event loop scopes is discouraged as it can make test code hard to follow. Test discovery modes ==================== diff --git a/docs/source/concepts_function_scope_example.py b/docs/source/concepts_function_scope_example.py new file mode 100644 index 00000000..1506ecf7 --- /dev/null +++ b/docs/source/concepts_function_scope_example.py @@ -0,0 +1,8 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio +async def test_runs_in_a_loop(): + assert asyncio.get_running_loop() diff --git a/docs/source/concepts_module_scope_example.py b/docs/source/concepts_module_scope_example.py new file mode 100644 index 00000000..66972888 --- /dev/null +++ b/docs/source/concepts_module_scope_example.py @@ -0,0 +1,17 @@ +import asyncio + +import pytest + +loop: asyncio.AbstractEventLoop + + +@pytest.mark.asyncio(scope="module") +async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + +@pytest.mark.asyncio(scope="module") +async def test_runs_in_a_loop(): + global loop + assert asyncio.get_running_loop() is loop diff --git a/docs/source/how-to-guides/class_scoped_loop_example.py b/docs/source/how-to-guides/class_scoped_loop_example.py new file mode 100644 index 00000000..5419a7ab --- /dev/null +++ b/docs/source/how-to-guides/class_scoped_loop_example.py @@ -0,0 +1,14 @@ +import asyncio + +import pytest + + +@pytest.mark.asyncio(scope="class") +class TestInOneEventLoopPerClass: + loop: asyncio.AbstractEventLoop + + async def test_remember_loop(self): + TestInOneEventLoopPerClass.loop = asyncio.get_running_loop() + + async def test_assert_same_loop(self): + assert asyncio.get_running_loop() is TestInOneEventLoopPerClass.loop diff --git a/docs/source/how-to-guides/index.rst b/docs/source/how-to-guides/index.rst index 71567aaf..a61ead50 100644 --- a/docs/source/how-to-guides/index.rst +++ b/docs/source/how-to-guides/index.rst @@ -5,6 +5,10 @@ How-To Guides .. toctree:: :hidden: + run_class_tests_in_same_loop + run_module_tests_in_same_loop + run_package_tests_in_same_loop + run_session_tests_in_same_loop multiple_loops uvloop test_item_is_async diff --git a/docs/source/how-to-guides/module_scoped_loop_example.py b/docs/source/how-to-guides/module_scoped_loop_example.py new file mode 100644 index 00000000..b4ef778c --- /dev/null +++ b/docs/source/how-to-guides/module_scoped_loop_example.py @@ -0,0 +1,17 @@ +import asyncio + +import pytest + +pytestmark = pytest.mark.asyncio(scope="module") + +loop: asyncio.AbstractEventLoop + + +async def test_remember_loop(): + global loop + loop = asyncio.get_running_loop() + + +async def test_assert_same_loop(): + global loop + assert asyncio.get_running_loop() is loop diff --git a/docs/source/how-to-guides/package_scoped_loop_example.py b/docs/source/how-to-guides/package_scoped_loop_example.py new file mode 100644 index 00000000..f48c33f1 --- /dev/null +++ b/docs/source/how-to-guides/package_scoped_loop_example.py @@ -0,0 +1,3 @@ +import pytest + +pytestmark = pytest.mark.asyncio(scope="package") diff --git a/docs/source/how-to-guides/run_class_tests_in_same_loop.rst b/docs/source/how-to-guides/run_class_tests_in_same_loop.rst new file mode 100644 index 00000000..a265899c --- /dev/null +++ b/docs/source/how-to-guides/run_class_tests_in_same_loop.rst @@ -0,0 +1,8 @@ +====================================================== +How to run all tests in a class in the same event loop +====================================================== +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="class")``. +This is easily achieved by using the *asyncio* marker as a class decorator. + +.. include:: class_scoped_loop_example.py + :code: python diff --git a/docs/source/how-to-guides/run_module_tests_in_same_loop.rst b/docs/source/how-to-guides/run_module_tests_in_same_loop.rst new file mode 100644 index 00000000..e07eca2e --- /dev/null +++ b/docs/source/how-to-guides/run_module_tests_in_same_loop.rst @@ -0,0 +1,8 @@ +======================================================= +How to run all tests in a module in the same event loop +======================================================= +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="module")``. +This is easily achieved by adding a `pytestmark` statement to your module. + +.. include:: module_scoped_loop_example.py + :code: python diff --git a/docs/source/how-to-guides/run_package_tests_in_same_loop.rst b/docs/source/how-to-guides/run_package_tests_in_same_loop.rst new file mode 100644 index 00000000..24326ed1 --- /dev/null +++ b/docs/source/how-to-guides/run_package_tests_in_same_loop.rst @@ -0,0 +1,11 @@ +======================================================== +How to run all tests in a package in the same event loop +======================================================== +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="package")``. +Add the following code to the ``__init__.py`` of the test package: + +.. include:: package_scoped_loop_example.py + :code: python + +Note that this marker is not passed down to tests in subpackages. +Subpackages constitute their own, separate package. diff --git a/docs/source/how-to-guides/run_session_tests_in_same_loop.rst b/docs/source/how-to-guides/run_session_tests_in_same_loop.rst new file mode 100644 index 00000000..7b0da918 --- /dev/null +++ b/docs/source/how-to-guides/run_session_tests_in_same_loop.rst @@ -0,0 +1,8 @@ +========================================================== +How to run all tests in the session in the same event loop +========================================================== +All tests can be run inside the same event loop by marking them with ``pytest.mark.asyncio(scope="session")``. +The easiest way to mark all tests is via a ``pytest_collection_modifyitems`` hook in the ``conftest.py`` at the root folder of your test suite. + +.. include:: session_scoped_loop_example.py + :code: python diff --git a/docs/source/how-to-guides/session_scoped_loop_example.py b/docs/source/how-to-guides/session_scoped_loop_example.py new file mode 100644 index 00000000..e06ffeb5 --- /dev/null +++ b/docs/source/how-to-guides/session_scoped_loop_example.py @@ -0,0 +1,10 @@ +import pytest + +from pytest_asyncio import is_async_test + + +def pytest_collection_modifyitems(items): + pytest_asyncio_tests = (item for item in items if is_async_test(item)) + session_scope_marker = pytest.mark.asyncio(scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker)