After finishing the dashboard, I hoped to quickly switch my datastore implementation to use the asynchronous methods for SQLAlchemy. Unfortunately, that was not the case. Everything needs to change, and that change must be done all at once. That includes pytest, that we need to run our tests.
To make the change more understandable, we start in this post with pytest and do the necessary work to test asynchronous methods. With that out of our way, we can next week focus on SQLAlchemy.
This post is part of my journey to learn Python. You find the code for this post in my PythonFriday repository on GitHub.
No native support for async methods
For our first steps with asynchronous tests in pytest, we can write a minimalistic test function that looks like this:
1 2 3 4 |
import asyncio async def test_function(): await asyncio.sleep(1) |
It may come as a surprise, but pytest does not support asynchronous functions directly. As soon as we add the async keyword in front of our test methods, we get this error:
PytestUnhandledCoroutineWarning: async def functions are not natively supported and have been skipped.
You need to install a suitable plugin for your async framework, for example:
– anyio
– pytest-asyncio
– pytest-tornasync
– pytest-trio
– pytest-twisted
warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid)))— Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
Install the support for asynchronous functions
We can solve this shortcoming with an additional package, like pytest-asyncio and install it with pip:
1 |
pip install pytest-asyncio |
Add the async decorator
If we run our test again, we get the same error. We must add a decorator to tell pytest that it should run that test asynchronously:
1 2 3 4 5 6 7 8 |
import asyncio import pytest import pytest_asyncio @pytest.mark.asyncio async def test_function(): await asyncio.sleep(1) |
With this additional line (and the import), pytest can finally run our test.
Asynchronous fixtures
There is one more problem waiting for us: fixtures. As long as we use a synchronous fixture with our asynchronous tests, everything works as expected. But as soon as we need to await something in the fixture, we get a new problem:
1 2 3 4 5 6 7 8 |
@pytest.fixture() async def prepare(): print("setup...") await asyncio.sleep(1) @pytest.mark.asyncio async def test_function_with_fixture(prepare): await asyncio.sleep(1) |
RuntimeWarning: coroutine ‘prepare’ was never awaited
item.funcargs = None # type: ignore[attr-defined]
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.— Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
We have an await in the fixture, but pytest does not know that this should be asynchronous. We can fix that problem with a different decorator:
1 2 3 4 5 6 7 8 9 |
# @pytest.fixture() @pytest_asyncio.fixture() async def prepare(): print("setup...") await asyncio.sleep(1) @pytest.mark.asyncio async def test_function_with_fixture(prepare): await asyncio.sleep(1) |
Now our test with the fixture can run without any errors or warnings.
Next
With an additional package and two different decorators we can get pytest to support asynchronous tests. All the errors have helpful messages that guide us to the solution. Nevertheless, it is a frustrating experience when you first run into this shortcoming. I hope this post helps you to shorten your learning curve.
With this infrastructure problem out of our way, we can next week switch to an asynchronous SQLAlchemy.
1 thought on “Python Friday #239: Asynchronous Tests With Pytest”