Skip to content

Running async tests in Python

Published: December 18, 2023. Filed under: Django, 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.