Skip to content

Commit e90ec4d

Browse files
authored
Restructure fixtures (#309)
Restructure fixtures to reduce per-test fixture overhead.
1 parent 058fea0 commit e90ec4d

4 files changed

Lines changed: 38 additions & 24 deletions

File tree

src/main.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import argparse
22
import asyncio
33
import sys
4-
from collections.abc import AsyncGenerator
4+
from collections.abc import AsyncIterator
55
from contextlib import asynccontextmanager
66
from pathlib import Path
77

@@ -32,10 +32,12 @@
3232

3333

3434
@asynccontextmanager
35-
async def lifespan(app: FastAPI | None) -> AsyncGenerator[None, None]: # noqa: ARG001
35+
async def lifespan(
36+
app: FastAPI | None, # noqa: ARG001 # parameter required by FastAPI/Starlette
37+
) -> AsyncIterator[None]:
3638
"""Manage application lifespan - startup and shutdown events."""
3739
yield
38-
asyncio.gather(
40+
await asyncio.gather(
3941
logger.complete(),
4042
close_databases(),
4143
)

src/routers/dependencies.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import AsyncGenerator
1+
from collections.abc import AsyncGenerator, AsyncIterator
22
from typing import Annotated
33

44
from fastapi import Depends
@@ -11,13 +11,13 @@
1111
from database.users import APIKey, User
1212

1313

14-
async def expdb_connection() -> AsyncGenerator[AsyncConnection, None]:
14+
async def expdb_connection() -> AsyncIterator[AsyncConnection]:
1515
engine = expdb_database()
1616
async with engine.connect() as connection, connection.begin():
1717
yield connection
1818

1919

20-
async def userdb_connection() -> AsyncGenerator[AsyncConnection, None]:
20+
async def userdb_connection() -> AsyncIterator[AsyncConnection]:
2121
engine = user_database()
2222
async with engine.connect() as connection, connection.begin():
2323
yield connection
@@ -26,7 +26,7 @@ async def userdb_connection() -> AsyncGenerator[AsyncConnection, None]:
2626
async def fetch_user(
2727
api_key: APIKey | None = None,
2828
user_data: Annotated[AsyncConnection | None, Depends(userdb_connection)] = None,
29-
) -> AsyncGenerator[User | None, None]:
29+
) -> AsyncGenerator[User | None]:
3030
if not (api_key and user_data):
3131
yield None
3232
return

tests/conftest.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import contextlib
22
import json
3-
from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator
3+
from collections.abc import AsyncIterator, Iterable, Iterator
44
from pathlib import Path
55
from typing import Any, NamedTuple
66

@@ -9,11 +9,13 @@
99
import pytest
1010
from _pytest.config import Config
1111
from _pytest.nodes import Item
12+
from asgi_lifespan import LifespanManager
13+
from fastapi import FastAPI
1214
from sqlalchemy import text
1315
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
1416

1517
from database.setup import expdb_database, user_database
16-
from main import create_api, lifespan
18+
from main import create_api
1719
from routers.dependencies import expdb_connection, userdb_connection
1820

1921
PHP_API_URL = "http://php-api:80/api/v1/json"
@@ -51,12 +53,6 @@ async def temporary_records(
5153
await connection.commit()
5254

5355

54-
@pytest.fixture(autouse=True, scope="session")
55-
async def one_lifespan() -> AsyncGenerator[None, None]:
56-
async with lifespan(app=None):
57-
yield
58-
59-
6056
@pytest.fixture
6157
async def expdb_test() -> AsyncIterator[AsyncConnection]:
6258
async with automatic_rollback(expdb_database()) as connection:
@@ -69,20 +65,34 @@ async def user_test() -> AsyncIterator[AsyncConnection]:
6965
yield connection
7066

7167

72-
@pytest.fixture
68+
# The PHP API fixture can be session scoped since they do not need access to
69+
# function-scoped database transactions.
70+
@pytest.fixture(scope="session")
7371
async def php_api() -> AsyncIterator[httpx.AsyncClient]:
7472
async with httpx.AsyncClient(base_url=PHP_API_URL) as client:
7573
yield client
7674

7775

76+
@pytest.fixture(scope="session")
77+
async def app() -> AsyncIterator[FastAPI]:
78+
_app = create_api(Path(__file__).parent / "config.test.toml")
79+
async with LifespanManager(_app):
80+
yield _app
81+
82+
7883
@pytest.fixture
7984
async def py_api(
80-
expdb_test: AsyncConnection, user_test: AsyncConnection
85+
expdb_test: AsyncConnection, user_test: AsyncConnection, app: FastAPI
8186
) -> AsyncIterator[httpx.AsyncClient]:
82-
app = create_api(Path(__file__).parent / "config.test.toml")
87+
"""Create test client which automatically rolls back database updates on teardown."""
88+
# Using the function-scoped database fixtures automatically benefits the
89+
# automatic rollbacks, but also lets a test author write to a database
90+
# transaction that is shared with the app. That is, it enables:
91+
#
92+
# def my_test(expdb_test, py_api):
93+
# expdb_test.execute(...) # write some data # noqa: ERA001
94+
# py_api.get(...) # read that data # noqa: ERA001
8395

84-
# We use async generator functions because fixtures may not be called directly.
85-
# The async generator returns the test connections for FastAPI to handle properly
8696
async def override_expdb() -> AsyncIterator[AsyncConnection]:
8797
yield expdb_test
8898

@@ -91,15 +101,17 @@ async def override_userdb() -> AsyncIterator[AsyncConnection]:
91101

92102
app.dependency_overrides[expdb_connection] = override_expdb
93103
app.dependency_overrides[userdb_connection] = override_userdb
94-
# We do not use the Lifespan manager for now because our auto-use fixture
95-
# `one_lifespan` will do setup and teardown at a session scope level instead.
104+
96105
async with httpx.AsyncClient(
97106
transport=httpx.ASGITransport(app=app),
98107
base_url="http://test",
99108
follow_redirects=True,
100109
) as client:
101110
yield client
102111

112+
app.dependency_overrides[expdb_connection] = expdb_connection
113+
app.dependency_overrides[userdb_connection] = userdb_connection
114+
103115

104116
@pytest.fixture
105117
def dataset_130() -> Iterator[dict[str, Any]]:

tests/routers/openml/migration/setups_migration_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import asyncio
22
import contextlib
33
import re
4-
from collections.abc import AsyncGenerator, Callable, Iterable
4+
from collections.abc import AsyncIterator, Callable, Iterable
55
from contextlib import AbstractAsyncContextManager
66
from http import HTTPStatus
77

@@ -22,7 +22,7 @@ def temporary_tags(
2222
@contextlib.asynccontextmanager
2323
async def _temporary_tags(
2424
tags: Iterable[str], setup_id: int, *, persist: bool = False
25-
) -> AsyncGenerator[None]:
25+
) -> AsyncIterator[None]:
2626
insert_queries = [
2727
(
2828
"INSERT INTO setup_tag(`id`,`tag`,`uploader`) VALUES (:setup_id, :tag, :user_id);",

0 commit comments

Comments
 (0)