Testing Async Python Code
Introduction
In software development the term asynchronous code means the code is non-blocking, it can be executed without waiting for it to complete, allowing you to carry on with other tasks. When you execute asynchronous code you never know for sure in which order the tasks will be executed. This is a complex topic when it comes to python implementation, and even more complex when it comes to actually testing its implementation in python.
The primary issue with asynchronous code in python is that the global interpreter lock (GIL) prevents efficient use of multiple CPU cores. However it does not impact the ability to have multiple active sockets operations running in parallel. What does this mean for you, the user? It simply means you can have an event queue running in a single thread that allows you to have open connections with many web servers, databases, etc.
Having all async operations performed on a single thread might seem strange, but this means we get all the advantages of multithreading (as opposed to a multiprocessor solution). Threads are lightweight, and all share a process memory pool. This makes the cost of communications between threads low, and makes task-switching much faster.
The following article shows how to use pytest and the asyncio package to use and test async functions in your python code.
Practical Examples
In this first example, we have a number of async tasks defined (identified by the async keyword), the third_party_service_call function mimics a nondeterministic external call by executing 3 subtasks that sleep for a random amount of time. The multi task function asynchronously calls the third_party_service_call function “n” number of times. We can see from the console outputs that task switching is occurring, the first two subtasks of task 1 are carried out, before switching to the first two subtasks of task 0, before switching back to subtask 3 of task 1, and finally subtask 3 of task 0.
The last line of the program references the event loop. The event loop is a core component of the Python async system. When the await keyword is encountered, a context switch is triggered. The event loop then looks for tasks waiting for an event and passes control to a task with an event that’s ready.

It’s easy to see that as the complexity and scale of a system grows, so too does the requirement for this type of code.
The next question is how do we test these functions? In this next code snippet we can attempt to call the asynchronous code as we would in any other test. The test immediately fails because the coroutine ‘third_party_service_call’ was never awaited.

This should be a simple fix, we simply add await to the test, this means that we must also add async to the test method signature, as the compiler will immediately complain if you attempt to use the await keyword outside of an async function.

So this time the interpreter was ok with the syntax, however the test was skipped because pytest skips coroutines by default. By following the instructions in the warnings, this problem can be addressed. Installing pytest-asyncio allows users to use the pytest.mark.asyncio decorator to mark tests as asynchronous.
Python 3.8 supports AsyncMock out-of-the-box in the unittest.mock module. This is the official python recommendation for mocking Async functions.

As your tests get more complex you may find yourself using asyncio more and more to test your code. To avoid decorating every test method, it’s a good idea to create a pytest.ini file and add asyncio_mode = auto into the pytest configuration.


This simple changes allows us to use async and await in our test files, making the tests easier to read, and without skipping tests or adding unsightly decorators

Conclusion
By now you should have a better understanding of what asynchronous programming is and how it is implemented in Python. By enabling programmatic control of context switches, async functions can significantly improve the performance of IO driven software solutions. This leads to some initial uncertainty around testing, but by following Python best practices and using recommended plugins such as asyncio, the tests for an async codebase look almost identical to more traditional synchronous tests.
Snowmate - Early Access
What if you had a solution that generates and maintains your entire backend unit tests 100% automatically? Well, it is now getting built and will soon be released.
If this interests you, sign up for our early access.