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.
A-sync-ing feeling
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 pytest-asyncio
or 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.