Testing async code

unittest · Advanced · 10 min

Async services need tests too, and plain TestCase cannot await.

unittest.IsolatedAsyncioTestCase (3.8+) runs each test in its own fresh event loop, so test methods can be async def and await freely.

class TestClient(unittest.IsolatedAsyncioTestCase):
    async def asyncSetUp(self): self.client = await connect()
    async def test_get(self): self.assertEqual(await self.client.get("k"), 1)
    async def asyncTearDown(self): await self.client.close()

Lifecycle: setUp, then asyncSetUp, the test, asyncTearDown, tearDown. self.addAsyncCleanup(coro_fn) registers async cleanup, and enterAsyncContext(cm) (3.11) enters an async context manager. At the end the loop is shut down and remaining tasks are cancelled. Since 3.13 a custom loop factory can be set with the loop_factory class attribute.

`AsyncMock` is a mock whose call returns an awaitable. Its return_value is what the await produces, and it records awaits: assert_awaited_once_with(...), await_count. patch automatically uses AsyncMock when the target is an async function, and MagicMock configures async magic methods (__aenter__, __aiter__) too.

A common bug: calling an AsyncMock without awaiting it. The test may pass while nothing was really checked; assert_awaited_* catches that.

Example

import asyncio, unittest
from unittest.mock import AsyncMock

async def fetch_price(client, symbol):
    data = await client.get(f"/price/{symbol}")
    return round(data["price"] * 1.2, 2)

class TestPrice(unittest.IsolatedAsyncioTestCase):
    async def asyncSetUp(self):
        self.client = AsyncMock()
        self.client.get.return_value = {"price": 10.0}

    async def test_adds_markup(self):
        self.assertEqual(await fetch_price(self.client, "ABC"), 12.0)
        self.client.get.assert_awaited_once_with("/price/ABC")

    async def test_timeout(self):
        self.client.get.side_effect = TimeoutError
        with self.assertRaises(TimeoutError):
            await asyncio.wait_for(fetch_price(self.client, "X"), 1)

unittest.main(exit=False)           # Ran 2 tests ... OK

Remember

  • IsolatedAsyncioTestCase gives each test its own event loop
  • Use asyncSetUp, asyncTearDown and addAsyncCleanup for async fixtures
  • AsyncMock returns awaitables; check with assert_awaited_once_with

Watch out for

  • Calling an AsyncMock without await and asserting assert_called; use assert_awaited to ensure it was awaited.

Check yourself

  1. What does await mock() return for m = AsyncMock(return_value=5)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/unittest.html#unittest.IsolatedAsyncioTestCase

Builds on

Unlocks

Nothing builds on this yet.