aiohttp Writing tests

There are several tools for testing Python code.

We show how to work with the most powerful and popular tool called pytest .

We need to install pytest itself and pytest-aiohttp plugin first:

pip install pytest
pip install pytest-aiohttp

Note

pytest-aiohttp is not compatible with another pupular plugin pytest-asyncio.

Making test asynchronous

Just use async def test_*() instead of plain def test_*(). pytest-aiohttp does all dirty work for you:

async def test_a() -> None:
    await asyncio.sleep(0.01)
    assert 1 == 2  # False

Testing server code

Let’s sligtly modify application code to accept a path to database explicitly:

async def init_app(db_path: Path) -> web.Application:
    app = web.Application()
    app["DB_PATH"] = db_path
    ...


async def init_db(app: web.Application) -> AsyncIterator[None]:
    sqlite_db = app["DB_PATH"]
    db = await aiosqlite.connect(sqlite_db)
    ...

After that we can use a separate isolated test database for running unit tests.

We need handy fixtures to initialize test DB:

@pytest.fixture
def db_path(tmp_path: Path) -> Path:
    path = tmp_path / "test_sqlite.db"
    try_make_db(path)
    return path


@pytest.fixture
async def db(db_path: Path) -> aiosqlite.Connection:
    """DB connection to access test DB directly from tests"""
    conn = await aiosqlite.connect(db_path)
    conn.row_factory = aiosqlite.Row
    yield conn
    await conn.close()

Note

Fixtures have function scope (default), that mean that every test gets a new fresh empty database.

Another fixtire instantiates our server and starts it on random local TCP port using aiohttp provided aiohttp_client fixture:

@pytest.fixture
async def client(aiohttp_client: Any, db_path: Path) -> _TestClient:
    app = await init_app(db_path)
    return await aiohttp_client(app)

Test posts list:

async def test_list_empty(client: _TestClient) -> None:
    resp = await client.get("/api")
    assert resp.status == 200, await resp.text()
    data = await resp.json()
    assert data == {"data": [], "status": "ok"}

Use db fixture to add a post before testing /api/{post_id} web-handler:

async def test_get_post(client: _TestClient, db: aiosqlite.Connection) -> None:
    async with db.execute(
        "INSERT INTO posts (title, text, owner, editor) VALUES (?, ?, ?, ?)",
        ["title", "text", "user", "user"],
    ) as cursor:
        post_id = cursor.lastrowid
    await db.commit()

    resp = await client.get(f"/api/{post_id}")
    assert resp.status == 200
    data = await resp.json()
    assert data == {
        "data": {
            "editor": "user",
            "id": "1",
            "owner": "user",
            "text": "text",
            "title": "title",
        },
        "status": "ok",
    }

Testing client with real server

Create a fixture to start a test server and Client instance:

@pytest.fixture
async def server(aiohttp_server: Any, db_path: Path) -> _TestServer:
    app = await init_app(db_path)
    return await aiohttp_server(app)


@pytest.fixture
async def client(server: _TestServer) -> Client:
    async with Client(server.make_url("/"), "test_user") as client:
        yield client

Use Client instance to test against running server:

async def test_get_post(client: Client, db: aiosqlite.Connection) -> None:
    post = await client.create("test title", "test text")

    async with db.execute(
        "SELECT title, text, owner, editor FROM posts WHERE id = ?", [post.id]
    ) as cursor:
        record = await cursor.fetchone()
        assert record["title"] == "test title"
        assert record["text"] == "test text"
        assert record["owner"] == "test_user"
        assert record["editor"] == "test_user"

Testing client with fake server

Assume we have no server code. We need to use fake server:

async def test_get_post(aiohttp_server: Any) -> None:
    async def handler(request: web.Request) -> web.Response:
        data = await request.json()
        assert data["title"] == "test title"
        assert data["text"] == "test text"
        return web.json_response(
            {
                "status": "ok",
                "data": {
                    "id": 1,
                    "title": "test title",
                    "text": "test text",
                    "owner": "test_user",
                    "editor": "test_user",
                },
            }
        )

    app = web.Application()
    app.add_routes([web.post("/api", handler)])
    server = await aiohttp_server(app)
    async with Client(server.make_url("/"), "test_user") as client:
        post = await client.create("test title", "test text")

        assert post.id == 1
        assert post.title == "test title"
        assert post.text == "test text"
        assert post.owner == "test_user"
        assert post.editor == "test_user"

Working with HTTPS

To test HTTPS proper SSL certificates are required.

Certificate is coupled with DNS, you don’t want to pay for testing certs.

There are two options: use pre-generated self-signed certificate pair of use awesome trustme library:

@pytest.fixture
def tls_certificate_authority() -> Any:
    return trustme.CA()


@pytest.fixture
def tls_certificate(tls_certificate_authority: Any) -> Any:
    return tls_certificate_authority.issue_server_cert("localhost",
                                                       "127.0.0.1",
                                                       "::1")


@pytest.fixture
def server_ssl_ctx(tls_certificate: Any) -> ssl.SSLContext:
    ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
    tls_certificate.configure_cert(ssl_ctx)
    return ssl_ctx


@pytest.fixture
def client_ssl_ctx(tls_certificate_authority: Any) -> ssl.SSLContext:
    ssl_ctx = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
    tls_certificate_authority.configure_trust(ssl_ctx)
    return ssl_ctx

Pass server_ssl_ctx and client_ssl_ctx as ssl parameter to test secured TCP connection instead of plain TCP.

Client mocking

There is aioresponses third-party library:

pip install aioresponses

Usage:

from aioresponses import aioresponses

async def test_request() -> None:
    with aioresponses() as mocked:
        mocked.get('http://example.com', status=200, body='test')
        session = aiohttp.ClientSession()
        resp = await session.get('http://example.com')

        assert resp.status == 200
        assert "test" == await resp.text()

TBD (by Andrew)

  • Writing tests for aiohttp client

  • Writing tests for aiohttp server

  • Tests using pytest and pytest-asyncio.