Testing aiohttp clients and servers

aiohttp · Practical · 11 min

Async network code is easy to get subtly wrong; fast in-process tests catch it without real servers.

aiohttp ships its own testing tools in aiohttp.test_utils:

  • TestServer(app) starts your web.Application on a random local port inside the current event loop.
  • TestClient(server) is a ClientSession bound to that server, so you call await client.get("/path") with relative URLs.
  • AioHTTPTestCase integrates them with unittest.IsolatedAsyncioTestCase.

With pytest, install pytest-aiohttp (which uses pytest-asyncio) and use the aiohttp_client fixture:

async def test_hello(aiohttp_client):
    client = await aiohttp_client(create_app())
    resp = await client.get("/")
    assert resp.status == 200

Fixtures also exist for raw servers (aiohttp_server, aiohttp_raw_server). Write an app factory function (create_app()) so each test gets a fresh app with its own state.

For testing client code, the same tools help: start a small fake web.Application with TestServer that mimics the external API (including errors and slow responses with asyncio.sleep), and point your client at server.make_url("/"). This tests real HTTP behaviour, including timeouts and headers. Libraries like aioresponses instead patch ClientSession to return canned responses without any server.

Remember that startup and cleanup signals run with TestServer, so app resources such as database pools are created and closed per test.

Example

import asyncio
from aiohttp import ClientSession, ClientTimeout, web
from aiohttp.test_utils import TestServer

async def get_price(session, base_url, sku):          # client code under test
    async with session.get(f"{base_url}/price/{sku}") as resp:
        resp.raise_for_status()
        return (await resp.json())["price"]

async def fake_price(request):                        # fake external API
    if request.match_info["sku"] == "slow":
        await asyncio.sleep(2)
    return web.json_response({"price": 9.5})
async def test():
    app = web.Application()
    app.router.add_get("/price/{sku}", fake_price)
    async with TestServer(app) as server:
        base = str(server.make_url("")).rstrip("/")
        async with ClientSession(timeout=ClientTimeout(total=0.5)) as s:
            assert await get_price(s, base, "abc") == 9.5
            try:
                await get_price(s, base, "slow")
            except TimeoutError:
                print("timeout handled")                 # timeout handled
asyncio.run(test())

Remember

  • TestServer + TestClient run apps in-process; pytest-aiohttp provides aiohttp_client
  • Use an app factory so each test gets fresh state
  • Fake external APIs with a small TestServer app to test client code

Watch out for

  • Sharing one module-level app with mutable state across tests.

Check yourself

  1. Why write a create_app() factory instead of a module-level app for tests?

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

Official docs: docs.aiohttp.org/en/stable/testing.html