Running async tests in Python
This is part of a series of posts I’m doing as a sort of Python/Django Advent calendar, offering a small tip or piece of information each day from the first Sunday of Advent through Christmas Eve. See the first post for an introduction.
Async Python can be useful in the right situation, but one of the tricky things about it is that it requires a bit more effort to run than normal synchronous Python, because you need an async event loop that can run and manage all your async functions.
This is especially something you’ll notice when testing, since async code basically has to have async tests (remember, it’s a syntax error to
await inside a non-
async code block).
If you’re using the standard-library
unittest module you don’t have to do too much extra work; you can group async tests into an async test case class, and then the standard
python -m unittest discover runner will find and run them for you. As noted in the docs,
unittest.main() in a file containing async test cases also works.
If you’re using Django, and Django’s standard testing tools, you also don’t need to do much extra work; asynchronous testing is supported on the standard Django test-case class, which provides an async HTTP client and async versions of its standard methods (prefixed with
a for async —
alogin() in place of
login(), etc.), and then
manage.py test will run your async tests just as easily as it runs synchronous tests.
Where things get tricky is with
pytest, which — as I write this — does not natively support running async functions as tests, so you’ll need a plugin. I’ve generally used the
pytest plugin shipped by
AnyIO, which provides a
pytest mark that can be applied at module or function level to mark and run async tests, but there’s also pytest-asyncio, which similarly provides a
pytest mark for async tests. Mostly this will be a matter of taste and of how much you’re committing to additional libraries on top of the base Python
asyncio setup —
AnyIO provides a bunch of additional async helpers, while
pytest-asyncio just does the one thing in its name.
Also it’s worth noting that although pytest-django provides
pytest equivalents of many of Django’s testing helpers, including the async helpers, it does not (as far as I’m aware) provide an async test runner plugin, so you’d still need another plugin like
AnyIO to run async Django tests with it.
Just note that running async
pytest plugins generally does not come with support for running async class-based tests (where normal synchronous
pytest can run
unittest.TestCase classes), and that if you’re going to use async
pytest fixtures you’ll probably want to make sure those fixtures will work with the
pytest helper plugin you’ve chosen.